<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>daun_jung.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 07 May 2026 08:27:06 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>daun_jung.log</title>
            <url>https://velog.velcdn.com/images/daun_jung/profile/8df68410-fac1-437b-9fdf-c8ca27019c70/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. daun_jung.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/daun_jung" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Spring Security - JWT 기반 로그인 완벽 이해하기]]></title>
            <link>https://velog.io/@daun_jung/Spring-Security-JWT-%EA%B8%B0%EB%B0%98-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@daun_jung/Spring-Security-JWT-%EA%B8%B0%EB%B0%98-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 07 May 2026 08:27:06 GMT</pubDate>
            <description><![CDATA[<h2 id="1단계-이론">1단계: 이론</h2>
<h3 id="인증authentication-vs-인가authorization-개념">인증(Authentication) vs 인가(Authorization) 개념</h3>
<blockquote>
<p><strong>인증(Authentication)</strong> : &quot;너 누구야?&quot;
사용자가 자신이 누구인지 증명하는 과정입니다
예시) 로그인 시 이메일 + 비밀번호를 입력 =&gt; 맞으면 인증 성공</p>
</blockquote>
<blockquote>
<p>*<em>인가(Authorization) *</em>: &quot;너 이거 해도 돼?&quot;
인증된 사용자가 특정 기능을 사용할 권한이 있는지 확인하는 과정입니다
예시) 일반 유저가 관리가 페이지 접근 시도 -&gt; 차단
인증 없이는 불가능 (로그인 먼저, 권한 체크는 그 다음)</p>
</blockquote>
<h3 id="쿠기세션토큰">쿠기/세션/토큰</h3>
<p><strong>쿠키 (Cookie)</strong>
브라우저에 저장되는 작은 데이터
요청마다 자동으로 서버에 전송됨
예시: sessionId=abc123</p>
<p><strong>세션 (Session)</strong>
로그인 상태를 서버 메모리에 저장
쿠키에는 세션 ID만 저장하고, 실제 정보는 서버가 보관
서버가 기억하는 방식</p>
<p><strong>토큰 (Token)</strong>
로그인 상태를 토큰 자체에 저장
서버는 토큰을 발급만 하고 저장하지 않음
클라이언트가 보관하고 요청마다 직접 전송</p>
<h3 id="세션-방식-vs-jwt-방식-비교">세션 방식 vs JWT 방식 비교</h3>
<p>그럼 왜 JWT 방식에 대해 배울까요?</p>
<blockquote>
<p><strong>세션 방식 흐름</strong></p>
</blockquote>
<ol>
<li>로그인 요청</li>
<li>서버: 세션 저장 (메모리에 {sessionId: &quot;abc&quot;, userId: 1})</li>
<li>클라이언트: 쿠키에 sessionId 저장</li>
<li>다음 요청마다 sessionId 쿠키 자동 전송</li>
<li>서버: sessionId로 메모리 조회 → 사용자 확인</li>
</ol>
<blockquote>
<p><strong>JWT 방식 흐름</strong></p>
</blockquote>
<ol>
<li>로그인 요청</li>
<li>서버: JWT 토큰 발급 (토큰 안에 userId 포함)</li>
<li>클라이언트: 토큰 저장 (localStorage 등)</li>
<li>다음 요청마다 Header에 토큰 직접 전송
Authorization: Bearer &lt;토큰&gt;</li>
<li>서버: 토큰 서명만 검증 → 사용자 확인 (DB/메모리 조회 없음)</li>
</ol>
<p>따라서 JWT는 세션 방식에 비해
서버에 상태 저장을 하지 않기 때문에 메모리 부담이 없고,
토큰만 검증하면 되기에 서버가 여러 대여도 문제 없다는 장점을 갖고 있습니다</p>
<h3 id="jwt-구조">JWT 구조</h3>
<table>
<thead>
<tr>
<th>구성요소</th>
<th>내용</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Header</strong></td>
<td>어떤 알고리즘으로 서명했는지</td>
<td><code>{ &quot;alg&quot;: &quot;HS256&quot; }</code></td>
</tr>
<tr>
<td><strong>Payload</strong></td>
<td>실제 데이터 (userId, role, 만료시간 등)</td>
<td><code>{ &quot;userId&quot;: 1, &quot;exp&quot;: 1234567890 }</code></td>
</tr>
<tr>
<td><strong>Signature</strong></td>
<td>Header + Payload를 Secret Key로 서명한 값</td>
<td>위변조 방지용</td>
</tr>
</tbody></table>
<p>(payload는 base64로 인코딩된 것이라 누구나 디코딩 가능합니다 따라서 비밀번호 같은 민감한 정보는 절대 넣으면 안 됩니다)</p>
<h3 id="access-token--refresh-token-개념">Access Token / Refresh Token 개념</h3>
<blockquote>
<p><strong>Access Token</strong>
실제 API 인증에 사용하는 토큰
만료시간 짧게 설정 (30분 ~ 1시간)
탈취당해도 빨리 만료되므로 피해 최소화</p>
</blockquote>
<blockquote>
<p><strong>Refresh Token</strong>
Access Token이 만료됐을 때 새로 발급받기 위한 토큰
만료시간 길게 설정 (7일 ~ 30일)
보통 DB에 저장해서 관리
Access Token 만료
    → Refresh Token으로 새 Access Token 요청
    → 서버: Refresh Token 검증 후 새 Access Token 발급</p>
</blockquote>
<h1 id="jwt-기반-로그인-구현-강의자료">JWT 기반 로그인 구현 강의자료</h1>
<p><a href="https://www.notion.so/JWT-35944d828e678095b8d5ff8f60d130d8?source=copy_link">(추가된 코드를 구별하기 어렵다면 여길 클릭하여 코드 참고해 주세요!)</a></p>
<h2 id="2단계-프로젝트-세팅">2단계: 프로젝트 세팅</h2>
<h3 id="의존성-추가">의존성 추가</h3>
<pre><code class="language-java">plugins {
    id &#39;java&#39;
    id &#39;org.springframework.boot&#39; version &#39;3.5.13&#39;
    id &#39;io.spring.dependency-management&#39; version &#39;1.1.7&#39;
}

group = &#39;com.likelion&#39;
version = &#39;0.0.1-SNAPSHOT&#39;
description = &#39;likelion-crud&#39;

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
    compileOnly &#39;org.projectlombok:lombok&#39;
    developmentOnly &#39;org.springframework.boot:spring-boot-devtools&#39;
    runtimeOnly &#39;com.mysql:mysql-connector-j&#39;
    annotationProcessor &#39;org.projectlombok:lombok&#39;
    testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39;
    testRuntimeOnly &#39;org.junit.platform:junit-platform-launcher&#39;

    //swagger세팅
    implementation &#39;org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.12&#39;

    // JWT 관련 의존성 세팅
    implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;
    implementation &#39;io.jsonwebtoken:jjwt-api:0.12.6&#39;
    runtimeOnly &#39;io.jsonwebtoken:jjwt-impl:0.12.6&#39;
    runtimeOnly &#39;io.jsonwebtoken:jjwt-jackson:0.12.6&#39;
}

tasks.named(&#39;test&#39;) {
    useJUnitPlatform()
}
</code></pre>
<h2 id="3단계-핵심-컴포넌트-구현">3단계: 핵심 컴포넌트 구현</h2>
<h3 id="secret-key-생성">Secret Key 생성</h3>
<p>이 토큰은 우리 서버가 만든 게 맞아! 라는 걸 증명하기 위해 서버만 아는 비밀번호를 secret key라고 합니다</p>
<p>따라서 토큰 발급 시 secret key로 서명을 만들어서 토큰에 포함시킵니다</p>
<p><strong>서명 검증 과정</strong></p>
<pre><code>클라이언트가 토큰을 보냄
↓
서버: Secret Key로 signature 재계산
↓
토큰의 signature와 비교
↓
일치 → 인증 성공   /   불일치  → 위변조 감지, 요청 거부</code></pre><p>터미널에서 아래 명령어를 실행해서 Secret Key를 생성해 주세요</p>
<pre><code class="language-java">openssl rand -base64 64</code></pre>
<p>만든 secret key를 아래와 같이 yml 파일에 넣어주세요</p>
<pre><code class="language-java">spring:
  datasource:
    url: jdbc:mysql://localhost:3306/likelion   # 연결할 MySQL 데이터베이스의 주소
    username: root                                   # 본인 MySQL 사용자 이름
    password: mysql0!!                                  # 본인 MySQL 비밀번호
    driver-class-name: com.mysql.cj.jdbc.Driver # MySQL 드라이버 클래스

  jpa:
    hibernate:
      ddl-auto: create     # 테이블 자동 생성 (create, update, validate, none 중 택 1)
    show-sql: true         # 실행되는 SQL을 콘솔에 출력
    properties:
      hibernate:
        format_sql: true   # SQL 쿼리를 보기 좋게 출력 (정렬됨)

logging:
  level:
    org.hibernate.SQL: debug                  # 실행되는 SQL 로그 출력
    org.hibernate.type.descriptor.sql: trace  # 바인딩된 파라미터 값 로그 출력

jwt:
  secret: nvuhAejTCtXEIlnsuSpPtLXzTY+KaCZmXxzuAzu/pyHhL5m1cqe8f2ELsUy1wYpsFXrtJxam67eH2w6i5ue+IQ==
  expiration: 1800000  # Access Token 만료시간 (30분)</code></pre>
<h3 id="jwtutil">JwtUtil</h3>
<p>이제 본격적으로 토큰을 생성하고, 파싱과 검증하는 코드를 먼저 구현해볼게요</p>
<pre><code class="language-java">package com.likelion.likelioncrud.auth;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;

@Component
public class JwtUtil {

    // application.yml의 jwt.secret 값을 자동으로 주입
    @Value(&quot;${jwt.secret}&quot;)
    private String secretKey;

    // application.yml의 jwt.expiration 값을 자동으로 주입
    @Value(&quot;${jwt.expiration}&quot;)
    private long expiration;

    // 토큰 생성
    public String generateToken(Long userId) {
        return Jwts.builder()
                .subject(String.valueOf(userId))                                       // payload에 userId 저장 (subject는 String 타입)
                .issuedAt(new Date())                                                  // 토큰 발급 시간
                .expiration(new Date(System.currentTimeMillis() + expiration))         // 토큰 만료 시간
                .signWith(getSigningKey())                                             // Secret Key로 서명
                .compact();                                                       // 토큰 문자열로 변환
    }

    // 토큰 파싱
    public Long getUserId(String token) {
        // subject에 저장된 userId를 String → Long으로 변환해서 반환
        return Long.parseLong(parseClaims(token).getSubject());
    }

    // 토큰 유효성 검증
    // 유효하면 true, 만료 or 위변조 등이면 false
    public boolean validateToken(String token) {
        try {
            parseClaims(token); // 파싱 시 만료, 위변조 등이면 예외 발생
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    //payload 파싱 (userId를 꺼내서 누가 보낸 요청인지 알기 위해서)
    private Claims parseClaims(String token) {
        return Jwts.parser()
                .verifyWith(getSigningKey()) // Secret Key로 서명 검증
                .build()
                .parseSignedClaims(token)   // 토큰 파싱
                .getPayload();              // payload(Claims) 반환
    }

     // application.yml의 secret 문자열을 SecretKey 객체로 변환
    private SecretKey getSigningKey() {
        // Base64로 인코딩된 secret 문자열을 디코딩해서 SecretKey 객체 생성
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}
</code></pre>
<h3 id="jwtfilter">JwtFilter</h3>
<p>만든 JwtUtil를 이용해 요청마다 토큰 검증 후 SecurityContext 저장하는 코드를 구현해 볼게요</p>
<pre><code class="language-java">package com.likelion.likelioncrud.auth;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Collections;

// OncePerRequestFilter: 하나의 요청에 대해 딱 한 번만 실행되는 필터
@Component
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

     // 토큰을 검증하고 SecurityContext에 인증 정보를 저장하는 역할
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        // 1. 요청 헤더에서 Authorization 값 추출
        // 클라이언트는 &quot;Authorization: Bearer &lt;토큰&gt;&quot; 형태로 요청을 보냄
        String authHeader = request.getHeader(&quot;Authorization&quot;);

        // 2. Authorization 헤더가 없거나 &quot;Bearer &quot;로 시작하지 않으면 토큰 검증 생략
        // (로그인, 회원가입 등 인증이 필요 없는 요청은 그냥 통과)
        if (authHeader == null || !authHeader.startsWith(&quot;Bearer &quot;)) {
            filterChain.doFilter(request, response); // 다음 필터로 넘김
            return;
        }

        // 3. 토큰 값만 추출 (앞 7글자 &quot;Bearer &quot; 제거)
        String token = authHeader.substring(7);

        // 4. 토큰 유효성 검증 (만료 여부, 위변조 여부 확인)
        if (jwtUtil.validateToken(token)) {

            // 5. 토큰에서 userId 추출
            Long userId = jwtUtil.getUserId(token);

            // 6. userId로 Authentication 객체 생성
            // - 첫 번째 인자(principal): 현재 로그인한 사용자 정보 (userId)
            // - 두 번째 인자(credentials): 비밀번호 (토큰 방식에서는 불필요하므로 null)
            // - 세 번째 인자(authorities): 권한 목록 (현재는 빈 리스트)
            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(userId, null, Collections.emptyList());

            // 7. 요청 정보(IP, 세션 등)를 Authentication에 추가
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            // 8. SecurityContext에 인증 정보 저장
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        // 9. 다음 필터로 넘김
        filterChain.doFilter(request, response);
    }
}
</code></pre>
<h3 id="securityconfig">SecurityConfig</h3>
<p>Filter 등록, 경로별 인가 설정을 하기 위한 코드를 구현해 봅시다</p>
<pre><code class="language-java">package com.likelion.likelioncrud.auth;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import jakarta.servlet.http.HttpServletResponse;

// @EnableWebSecurity: Spring Security 활성화
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtFilter jwtFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // CSRF(Cross-Site Request Forgery) 보호 비활성화
                // JWT는 stateless 방식이라 세션/쿠키를 사용하지 않으므로 CSRF 불필요
                .csrf(AbstractHttpConfigurer::disable)

                // JWT 방식은 서버에 세션을 저장하지 않으므로 세션 사용 안 함
                .sessionManagement(session -&gt;
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

                // 경로별 인가(Authorization) 설정
                .authorizeHttpRequests(auth -&gt; auth

                        // 아래 경로들은 토큰 없이 누구나 접근 가능
                        .requestMatchers(
                                &quot;/auth/signup&quot;,      // 회원가입 API
                                &quot;/auth/login&quot;,       // 로그인 API
                                &quot;/swagger-ui/**&quot;,    // Swagger UI 페이지
                                &quot;/v3/api-docs/**&quot;    // Swagger API 문서
                        ).permitAll()

                        // 위에서 허용한 경로 외 나머지는 모두 인증(토큰) 필요
                        .anyRequest().authenticated()
                )

                // JwtFilter를 UsernamePasswordAuthenticationFilter 앞에 등록
                // → 모든 요청에서 컨트롤러 도달 전에 JWT 검증이 먼저 실행됨
                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
</code></pre>
<h2 id="4단계-회원가입--로그인-api-구현">4단계: 회원가입 / 로그인 API 구현</h2>
<h3 id="member">Member</h3>
<pre><code class="language-java">package com.likelion.likelioncrud.member.domain;

import com.likelion.likelioncrud.member.api.dto.request.MemberUpdateRequestDto;
import com.likelion.likelioncrud.post.domain.Post;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;member_id&quot;)
    private Long memberId;

    private String name;

    private int age;

    @Enumerated(EnumType.STRING)
    @Column(length = 20)
    private Part part;

    // 로그인 시 사용하는 이메일 (중복 불가)
    @Column(unique = true)
    private String email;

    // BCrypt로 암호화된 비밀번호 저장
    private String password;

    @OneToMany(mappedBy = &quot;member&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
    private List&lt;Post&gt; posts = new ArrayList&lt;&gt;();

    @Builder
    private Member(String name, int age, Part part, String email, String password) {
        this.name = name;
        this.age = age;
        this.part = part;
        this.email = email;
        this.password = password;
    }

    public void update(MemberUpdateRequestDto memberUpdateRequestDto) {
        this.name = memberUpdateRequestDto.name();
        this.age = memberUpdateRequestDto.age();
    }
}</code></pre>
<h3 id="memberrepository">MemberRepository</h3>
<pre><code class="language-java">package com.likelion.likelioncrud.member.domain.repository;

import com.likelion.likelioncrud.member.domain.Member;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
    Page&lt;Member&gt; findAll(Pageable pageable);

    // 이메일로 회원 조회 (로그인 시 사용)
    Optional&lt;Member&gt; findByEmail(String email);

    // 이메일 중복 체크 (회원가입 시 사용)
    boolean existsByEmail(String email);
}</code></pre>
<h3 id="errorcode">ErrorCode</h3>
<pre><code class="language-java">package com.likelion.likelioncrud.common.response.code;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter // getter 메소드 자동 생성 lombok 어노테이션
@AllArgsConstructor(access = AccessLevel.PRIVATE) // 모든 필드를 파라미터로 받는 생성자 자동 생성 어노테이션
public enum ErrorCode {

    /**
     * 404 NOT FOUND (찾을 수 없음)
     */
    MEMBER_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, &quot;해당 사용자가 없습니다. memberId = &quot;),
    POST_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, &quot;해당 게시글이 없습니다. postId = &quot;),

    /**
     * 400 BAD REQUEST
     */
    VALIDATION_EXCEPTION(HttpStatus.BAD_REQUEST, &quot;유효성 검사에 실패하였습니다 - &quot;),

    /**
     * 500 INTERNAL SERVER ERROR (내부 서버 에러)
     */
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, &quot;내부 서버 에러가 발생했습니다&quot;),

    // 로그인
    DUPLICATE_EMAIL_EXCEPTION(HttpStatus.BAD_REQUEST, &quot;이미 사용중인 이메일입니다.&quot;),
    INVALID_PASSWORD_EXCEPTION(HttpStatus.BAD_REQUEST, &quot;비밀번호가 일치하지 않습니다.&quot;),
    MEMBER_NOT_FOUND_BY_EMAIL_EXCEPTION(HttpStatus.UNAUTHORIZED, &quot;이메일을 찾을 수 없습니다.&quot;);

    private final HttpStatus httpStatus;    // HTTP 상태 코드를 스프링에서 쉽게 작성하기 위한 enum값들의 모임
    private final String message;           // 에러 메세지

    public int getHttpStatusCode() {        // HTTP 상태 코드에서 404와 같은 숫자 값만 반환해 주기 위한 메소드
        return httpStatus.value();
    }
}
</code></pre>
<h3 id="sucesscode">SucessCode</h3>
<pre><code class="language-java">package com.likelion.likelioncrud.common.response.code;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE) // 모든 필드를 파라미터로 받는 생성자 자동 생성 어노테이션
public enum SuccessCode {

    /**
     * 200 OK (성공)
     */
    GET_SUCCESS(HttpStatus.OK, &quot;성공적으로 조회했습니다.&quot;),
    MEMBER_UPDATE_SUCCESS(HttpStatus.OK, &quot;사용자가 성공적으로 수정되었습니다.&quot;),
    POST_UPDATE_SUCCESS(HttpStatus.OK, &quot;글이 성공적으로 수정되었습니다.&quot;),
    MEMBER_DELETE_SUCCESS(HttpStatus.OK, &quot;사용자가 성공적으로 삭제되었습니다.&quot;),
    POST_DELETE_SUCCESS(HttpStatus.OK, &quot;글이 성공적으로 삭제되었습니다.&quot;),

    /**
     * 201 CREATED (생성 성공)
     */
    MEMBER_SAVE_SUCCESS(HttpStatus.CREATED, &quot;사용자가 성공적으로 생성되었습니다.&quot;),
    POST_SAVE_SUCCESS(HttpStatus.CREATED, &quot;글이 성공적으로 생성되었습니다.&quot;),
    SIGNUP_SUCCESS(HttpStatus.CREATED, &quot;회원가입이 성공적으로 완료되었습니다.&quot;),

    // 로그인
    LOGIN_SUCCESS(HttpStatus.OK, &quot;로그인이 성공적으로 완료되었습니다.&quot;);

    private final HttpStatus httpStatus;   // HTTP 상태 코드를 스프링에서 쉽게 작성하기 위한 enum값들의 모임
    private final String message;          // 응답 메세지

    public int getHttpStatusCode() {       // HTTP 상태 코드에서 404와 같은 숫자 값만 반환해 주기 위한 메소드
        return httpStatus.value();
    }
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/1fc42b56-e899-4474-836a-e0930749ad04/image.png" alt=""></p>
<h3 id="signuprequestdto">SignupRequestDto</h3>
<pre><code class="language-java">package com.likelion.likelioncrud.auth.api.dto.request;

// 회원가입 요청 시 클라이언트에서 받는 데이터
public record SignupRequestDto(
        String name,
        String email,
        String password
) {
}</code></pre>
<h3 id="loginrequestdto">LoginRequestDto</h3>
<pre><code class="language-java">package com.likelion.likelioncrud.auth.api.dto.request;

// 로그인 요청 시 클라이언트에서 받는 데이터
public record LoginRequestDto(
        String email,
        String password
) {
}</code></pre>
<h3 id="loginresponsedto">LoginResponseDto</h3>
<pre><code class="language-java">package com.likelion.likelioncrud.auth.api.dto.response;

// 로그인 성공 시 클라이언트에게 반환하는 데이터
public record LoginResponseDto(
        String accessToken  // 발급된 JWT Access Token
) {
}</code></pre>
<h3 id="authservice">AuthService</h3>
<pre><code class="language-java">package com.likelion.likelioncrud.auth.application;

import com.likelion.likelioncrud.auth.JwtUtil;
import com.likelion.likelioncrud.auth.api.dto.request.LoginRequestDto;
import com.likelion.likelioncrud.auth.api.dto.request.SignupRequestDto;
import com.likelion.likelioncrud.auth.api.dto.response.LoginResponseDto;
import com.likelion.likelioncrud.common.exception.BusinessException;
import com.likelion.likelioncrud.common.response.code.ErrorCode;
import com.likelion.likelioncrud.member.domain.Member;
import com.likelion.likelioncrud.member.domain.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)  // 기본적으로 읽기 전용 트랜잭션 적용 (조회 성능 최적화)
public class AuthService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;  // SecurityConfig에서 빈으로 등록한 BCryptPasswordEncoder
    private final JwtUtil jwtUtil;

    // 회원가입
    @Transactional  // DB에 저장하는 작업이므로 쓰기 트랜잭션 적용
    public void signup(SignupRequestDto request) {

        // 1. 이메일 중복 체크
        if (memberRepository.existsByEmail(request.email())) {
            throw new BusinessException(ErrorCode.DUPLICATE_EMAIL_EXCEPTION, ErrorCode.DUPLICATE_EMAIL_EXCEPTION.getMessage());
        }

        // 2. 비밀번호 BCrypt 암호화 후 Member 생성
        Member member = Member.builder()
                .name(request.name())
                .email(request.email())
                .password(passwordEncoder.encode(request.password()))  // 비밀번호 암호화
                .build();

        // 3. DB 저장
        memberRepository.save(member);
    }

    // 로그인
    public LoginResponseDto login(LoginRequestDto request) {

        // 1. 이메일로 회원 조회 (없으면 예외 처리)
        Member member = memberRepository.findByEmail(request.email())
                .orElseThrow(() -&gt; new BusinessException(ErrorCode.MEMBER_NOT_FOUND_BY_EMAIL_EXCEPTION, ErrorCode.MEMBER_NOT_FOUND_BY_EMAIL_EXCEPTION.getMessage()));

        // 2. 입력한 비밀번호와 암호화된 비밀번호 비교
        if (!passwordEncoder.matches(request.password(), member.getPassword())) {
            throw new BusinessException(ErrorCode.INVALID_PASSWORD_EXCEPTION, ErrorCode.INVALID_PASSWORD_EXCEPTION.getMessage());
        }

        // 3. 인증 성공 → userId로 JWT 토큰 발급
        String accessToken = jwtUtil.generateToken(member.getMemberId());

        return new LoginResponseDto(accessToken);
    }
}</code></pre>
<h3 id="authcontroller">AuthController</h3>
<pre><code class="language-java">package com.likelion.likelioncrud.auth.api;

import com.likelion.likelioncrud.auth.api.dto.request.LoginRequestDto;
import com.likelion.likelioncrud.auth.api.dto.request.SignupRequestDto;
import com.likelion.likelioncrud.auth.api.dto.response.LoginResponseDto;
import com.likelion.likelioncrud.auth.application.AuthService;
import com.likelion.likelioncrud.common.response.code.SuccessCode;
import com.likelion.likelioncrud.common.template.ApiResTemplate;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/auth&quot;)
@Tag(name = &quot;로그인 API&quot;, description = &quot;회원가입, 로그인 관련 API&quot;)
public class AuthController {

    private final AuthService authService;

    // 회원가입
    @PostMapping(&quot;/signup&quot;)
    public ApiResTemplate&lt;Void&gt; signup(@RequestBody SignupRequestDto request) {
        authService.signup(request);
        return ApiResTemplate.successWithNoContent(SuccessCode.SIGNUP_SUCCESS);
    }

    // 로그인
    @PostMapping(&quot;/login&quot;)
    public ApiResTemplate&lt;LoginResponseDto&gt; login(@RequestBody LoginRequestDto request) {
        LoginResponseDto response = authService.login(request);
        return ApiResTemplate.successResponse(SuccessCode.LOGIN_SUCCESS, response); // 200 OK + accessToken
    }
}
</code></pre>
<h2 id="5단계-인증이-필요한-api-구현">5단계: 인증이 필요한 API 구현</h2>
<p>토큰 있고 없고의 차이를 직접 눈으로 확인하기 위해 실제로 적용해 봅시다!</p>
<h3 id="memberservice">MemberService</h3>
<pre><code class="language-java">package com.likelion.likelioncrud.member.application;

import com.likelion.likelioncrud.common.exception.BusinessException;
import com.likelion.likelioncrud.common.response.code.ErrorCode;
import com.likelion.likelioncrud.member.api.dto.request.MemberSaveRequestDto;
import com.likelion.likelioncrud.member.api.dto.request.MemberUpdateRequestDto;
import com.likelion.likelioncrud.member.api.dto.response.MemberInfoResponseDto;
import com.likelion.likelioncrud.member.domain.Member;
import com.likelion.likelioncrud.member.domain.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberService {

    private final MemberRepository memberRepository;

    // 내 정보 조회 (JWT 토큰에서 추출한 userId로 조회)
    public MemberInfoResponseDto memberFindMe(Long userId) {
        Member member = memberRepository.findById(userId)
                .orElseThrow(() -&gt; new BusinessException(
                        ErrorCode.MEMBER_NOT_FOUND_EXCEPTION,
                        ErrorCode.MEMBER_NOT_FOUND_EXCEPTION.getMessage() + userId
                ));
        return MemberInfoResponseDto.from(member);
    }

    // 사용자 정보 저장
    @Transactional
    public void memberSave(MemberSaveRequestDto memberSaveRequestDto) {
        Member member = Member.builder()
                .name(memberSaveRequestDto.name())
                .age(memberSaveRequestDto.age())
                .part(memberSaveRequestDto.part())
                .build();
        memberRepository.save(member);
    }

    // 사용자 모두 조회
    public Page&lt;MemberInfoResponseDto&gt; memberFindAll(Pageable pageable) {
        Page&lt;Member&gt; members = memberRepository.findAll(pageable);
        return members.map(MemberInfoResponseDto::from);
    }

    // 단일 사용자 조회
    public MemberInfoResponseDto memberFindOne(Long memberId) {
        Member member = memberRepository.findById(memberId)
                .orElseThrow(() -&gt; new BusinessException(
                        ErrorCode.MEMBER_NOT_FOUND_EXCEPTION,
                        ErrorCode.MEMBER_NOT_FOUND_EXCEPTION.getMessage() + memberId
                ));

        return MemberInfoResponseDto.from(member);
    }

    // 사용자 정보 수정
    @Transactional
    public void memberUpdate(Long memberId, MemberUpdateRequestDto memberUpdateRequestDto) {
        Member member = memberRepository.findById(memberId)
                .orElseThrow(() -&gt; new BusinessException(
                        ErrorCode.MEMBER_NOT_FOUND_EXCEPTION,
                        ErrorCode.MEMBER_NOT_FOUND_EXCEPTION.getMessage() + memberId));
        member.update(memberUpdateRequestDto);
    }

    // 사용자 정보 삭제
    @Transactional
    public void memberDelete(Long memberId) {
        Member member = memberRepository.findById(memberId)
                .orElseThrow(() -&gt; new BusinessException(
                        ErrorCode.MEMBER_NOT_FOUND_EXCEPTION,
                        ErrorCode.MEMBER_NOT_FOUND_EXCEPTION.getMessage() + memberId));
        memberRepository.delete(member);
    }
}</code></pre>
<h3 id="membercontroller">MemberController</h3>
<pre><code class="language-java">package com.likelion.likelioncrud.member.api;

import com.likelion.likelioncrud.common.response.code.SuccessCode;
import com.likelion.likelioncrud.common.template.ApiResTemplate;
import com.likelion.likelioncrud.member.api.dto.request.MemberSaveRequestDto;
import com.likelion.likelioncrud.member.api.dto.request.MemberUpdateRequestDto;
import com.likelion.likelioncrud.member.api.dto.response.MemberInfoResponseDto;
import com.likelion.likelioncrud.member.application.MemberService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/member&quot;)
@Tag(name = &quot;멤버 API&quot;, description = &quot;멤버 관리하는 api &quot;)
public class MemberController {

    private final MemberService memberService;

    // 내 정보 조회 (토큰 필요)
    // JwtFilter에서 SecurityContext에 저장한 userId를 꺼냄
    // 토큰 없이 요청하면 401 Unauthorized 반환
    @GetMapping(&quot;/me&quot;)
    @Operation(summary = &quot;내 정보 조회&quot;, description = &quot;JWT 토큰으로 현재 로그인한 사용자의 정보를 조회합니다.&quot;)
    public ApiResTemplate&lt;MemberInfoResponseDto&gt; memberFindMe(
            @AuthenticationPrincipal Long userId) {
        MemberInfoResponseDto response = memberService.memberFindMe(userId);
        return ApiResTemplate.successResponse(SuccessCode.GET_SUCCESS, response);
    }

    // 사용자 저장
    @PostMapping()
    @Operation(summary = &quot;멤버 회원가입&quot;, description = &quot;멤버 회원가입 설명란입니다.&quot;)
    public ApiResTemplate&lt;Void&gt; memberSave(@RequestBody @Valid MemberSaveRequestDto memberSaveRequestDto) {
        memberService.memberSave(memberSaveRequestDto);
        return ApiResTemplate.successWithNoContent(SuccessCode.MEMBER_SAVE_SUCCESS);
    }

    // 사용자 전체 조회
    @GetMapping(&quot;/all&quot;)
    @Operation(summary = &quot;멤버 전체조회&quot;, description = &quot;멤버 전체조회&quot;)
    public ApiResTemplate&lt;Page&lt;MemberInfoResponseDto&gt;&gt; memberFindAll(
            @ParameterObject
            @PageableDefault(
                    size = 10,
                    sort = &quot;memberId&quot;,
                    direction = Sort.Direction.ASC
            ) Pageable pageable
    ) {
        Page&lt;MemberInfoResponseDto&gt; members = memberService.memberFindAll(pageable);
        return ApiResTemplate.successResponse(SuccessCode.GET_SUCCESS,members);
    }

    // 회원 id를 통해 특정 사용자 조회
    @GetMapping(&quot;/{memberId}&quot;)
    @Operation(summary = &quot;멤버 1명 조회&quot;, description = &quot;멤버 id로 멤버조회&quot;)
    public ApiResTemplate&lt;MemberInfoResponseDto&gt; memberFindOne(@PathVariable(&quot;memberId&quot;) Long memberId) {
        MemberInfoResponseDto memberInfoResponseDto = memberService.memberFindOne(memberId);
        return ApiResTemplate.successResponse(SuccessCode.GET_SUCCESS, memberInfoResponseDto);
    }

    // 회원 id를 통한 사용자 수정
    @PatchMapping(&quot;/{memberId}&quot;)
    @Operation(summary = &quot;멤버 업데이트&quot;, description = &quot;멤버 업데이트&quot;)
    public ApiResTemplate&lt;Void&gt; memberUpdate(@PathVariable(&quot;memberId&quot;) Long memberId,
                                             @RequestBody MemberUpdateRequestDto memberUpdateRequestDto) {
        memberService.memberUpdate(memberId, memberUpdateRequestDto);
        return ApiResTemplate.successWithNoContent(SuccessCode.MEMBER_UPDATE_SUCCESS);
    }

    // 회원 id를 통한 사용자 삭제
    @DeleteMapping(&quot;/{memberId}&quot;)
    @Operation(summary = &quot;멤버 삭제&quot;, description = &quot;멤버 삭제&quot;)
    public ApiResTemplate&lt;Void&gt; memberDelete(@PathVariable(&quot;memberId&quot;) Long memberId) {
        memberService.memberDelete(memberId);
        return ApiResTemplate.successWithNoContent(SuccessCode.MEMBER_DELETE_SUCCESS);
    }
}
</code></pre>
<h2 id="6단계-swagger-테스트">6단계: Swagger 테스트</h2>
<p>이제 테스트를 해봅시다!</p>
<p>서버 실행하고 스웨거 접속해 주세요</p>
<pre><code class="language-java">http://localhost:8080/swagger-ui/index.html</code></pre>
<p>로그인을 하지 않고 내 정보 조회를 해보면 토큰이 없어 당연히 401 에러가 뜹니다
<img src="https://velog.velcdn.com/images/daun_jung/post/572f838e-52a3-4926-908c-7f60434b7a66/image.png" alt="">
이제 회원가입을 해주세요</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/8fdb141a-cbfd-4692-85a9-73781c634ba6/image.png" alt=""></p>
<p>그 다음 로그인 후 토큰을 발급 받아주세요</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/14b24563-9a9f-4613-bcf3-f15e93f2ad2d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/a084ade6-d73b-4856-83cf-0c2342b7fa26/image.png" alt=""></p>
<p>발급받은 accessToken를 상단 왼쪽 Authorize 버튼 클릭 후 붙여 넣어주세요</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/af821e48-0599-4c8e-b909-48fdc40e6c25/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/997b8632-4656-46f3-a742-f7458face935/image.png" alt=""></p>
<p>성공했다면 아래와 같이 내 정보 조회가 성공적으로 이뤄집니다!</p>
<h2 id="과제-😆">과제 😆</h2>
<h3 id="1-구현할-내용">1. 구현할 내용</h3>
<p>강의에서 다룬 JWT 기반 로그인에 <strong>Refresh Token</strong> 기능을 추가로 구현하세요!</p>
<p>뼈대 코드가 이미 제공되어 있으니, // TODO 주석이 달린 부분만 채우면 됩니다!</p>
<p>수정할 부분은 제가 보내드린 과제 프로젝트에서 JwtUtil.java와 AuthService.java 두 개뿐입니다~
<img src="https://velog.velcdn.com/images/daun_jung/post/90723cde-9ca6-4636-8273-b7287ab6d3c7/image.png" alt="">
(이런식으로 추가되어있습니다!)</p>
<p>⚠️ 시작 전 필수 설정
src/main/resources/application.yml 파일을 본인이 쓰던 파일로 수정하세요!!</p>
<blockquote>
<p>AI 사용은 허용하지만 AI가 작성한 코드에는 반드시 본인이 직접 주석을 달아야 합니다! 주석 없이 제출한 경우 가이드라인 미준수입니다ㅠㅠ</p>
</blockquote>
<pre><code class="language-java">// 주석 예시
// 기존에 저장된 이 사용자의 refresh token을 삭제
// 재로그인 시 이전 refresh token이 남아있으면 안 되기 때문
refreshTokenRepository.deleteByMemberId(member.getMemberId());</code></pre>
<h3 id="2--pr-description-작성-항목">2.  PR Description 작성 항목</h3>
<blockquote>
<ol>
<li>Refresh Token이 필요한 이유
Access Token만 사용할 때의 문제점과 Refresh Token이 이를 어떻게 해결하는지 설명해 주세요!</li>
</ol>
</blockquote>
<blockquote>
<ol start="2">
<li>구현 흐름 설명
로그인 시 Refresh Token이 어떻게 발급되고 저장되는지 설명해 주세요!</li>
</ol>
</blockquote>
<blockquote>
<ol start="3">
<li>Swagger 테스트 스크린샷
POST /auth/login    응답에 accessToken, refreshToken 둘 다 포함되는지
POST /auth/refresh    refresh token 전달 시 새 accessToken 발급되는지
POST /auth/refresh (실패 케이스)    잘못된 토큰 전달 시 401 응답이 오는지</li>
</ol>
</blockquote>
<p>⏰ 제출 기한
5월 23일 오후 11시 59분까지 PR 올려주세요~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[S3 이미지 업로드 완벽 이해하기]]></title>
            <link>https://velog.io/@daun_jung/S3-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@daun_jung/S3-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 06 May 2026 07:08:24 GMT</pubDate>
            <description><![CDATA[<h1 id="이론">이론</h1>
<h3 id="이미지-업로드가-뭔가요">이미지 업로드가 뭔가요?</h3>
<p>우리가 일상에서 쓰는 서비스들 중 </p>
<ul>
<li>카카오톡에서 프로필 사진 바꾸기</li>
<li>인스타그램에서 사진 게시물 올리기
이 모든 게 이미지 업로드에요!</li>
</ul>
<p>사용자 입장에서는 그냥 사진 올리기 버튼이지만 백엔드 입장에서는 이런 일이 일어납니다</p>
<blockquote>
<p>사용자 기기 안에 있는 이미지 파일
        ↓
네트워크를 통해 서버로 전송
        ↓
서버가 파일을 어딘가에 저장
        ↓
나중에 꺼내 쓸 수 있도록 URL로 관리</p>
</blockquote>
<h3 id="왜-s3를-쓰나요">왜 S3를 쓰나요?</h3>
<p>서버에 이미지를 직접 저장하면 안되냐고 생각할 수 있어요</p>
<p>하지만 서버에 직접 저장하면 생기는 문제가 있답니다</p>
<ul>
<li>서버가 여러대면 어떤 서버에 이미지가 있는지 알 수 없어요</li>
<li>서버가 죽으면 이미지도 같이 날아가요</li>
<li>서버 디스크 용량이 한계가 있어요</li>
</ul>
<p>따라서 S3를 쓰면</p>
<ul>
<li>이미지 저장 공간을 서버와 완전히 분리할 수 있고</li>
<li>사실상 S3의 용량은 제한이 없답니다</li>
<li>이미지 URL 하나로 어디서든 접근 가능합니다</li>
</ul>
<h3 id="s3-핵심-용어-3가지">S3 핵심 용어 3가지</h3>
<h4 id="bucket">Bucket</h4>
<p>쉽게 말해 폴더입니다
이미지들을 담을 최상위 저장 공간이에요
버킷 이름은 전 세계에서 유일해야 한답니다</p>
<h4 id="object">Object</h4>
<p>버킷 안에 저장된 파일 하나하나를 오브젝트라고 합니다
이미지, 영상, 문서 등 모든 파일이 오브젝트에요</p>
<h4 id="key">Key</h4>
<p>오브젝트의 경로 + 파일명이에요
예) images/profile/uuid-123.jpg이 Key로 파일을 식별합니다</p>
<h3 id="업로드-전체-흐름">업로드 전체 흐름</h3>
<blockquote>
<p>① 클라이언트가 이미지를 서버로 전송
        ↓
② 서버가 파일을 받아서 S3에 업로드
        ↓
③ S3가 저장 후 접근 가능한 URL 생성
        ↓
④ 서버가 URL을 DB에 저장하고 클라이언트에 응답</p>
</blockquote>
<p>클라이언트는 이미지를 직접 가지고 다니는 게 아니라 URL만 저장하고 필요할 때 URL로 접근하는 구조랍니다</p>
<h1 id="실습">실습</h1>
<h3 id="1-s3-버킷-생성">1. S3 버킷 생성</h3>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/122f6c57-e82c-4a7f-a530-429f4727a9e7/image.png" alt="">
s3 검색하고 들어가 줍니다
<img src="https://velog.velcdn.com/images/daun_jung/post/eb8e226a-76e0-467f-99cd-554532e81282/image.png" alt="">
버킷 만들기 버튼을 눌러줍니다
<img src="https://velog.velcdn.com/images/daun_jung/post/c4412894-4795-4278-a757-fed0abdc6a79/image.png" alt="">
버킷 이름을 likelion-이름 으로 만들어 줍니다
<img src="https://velog.velcdn.com/images/daun_jung/post/40eb42b6-2ff4-48eb-8f81-38ff79e392dd/image.png" alt="">
퍼플릭 액세스 차단을 모두 해제해주고 동의에 체크해 줍니다
<img src="https://velog.velcdn.com/images/daun_jung/post/ae48a675-ab36-4d5b-bf5a-96fb23f5d2c2/image.png" alt="">
나머지는 건들지 말고 버킷 만들기를 클릭합니다</p>
<h3 id="2-권한-편집">2. 권한 편집</h3>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/d70aa81b-247c-4c4e-a803-0d4060afe134/image.png" alt="">
만든 버킷의 권한 섹터로 들어와 버킷 정책의 편집을 클릭해 줍니다
<img src="https://velog.velcdn.com/images/daun_jung/post/68a62234-33e5-4441-928f-1402ee72ad93/image.png" alt="">
들어와서 새 문 추가를 클릭해 줍니다
<img src="https://velog.velcdn.com/images/daun_jung/post/bd11fbd9-3a37-4ced-adcc-dd6036c263e2/image.png" alt="">
서비스 선택에 S3를 검색하고 제일 위에 것을 클릭합니다
<img src="https://velog.velcdn.com/images/daun_jung/post/da6fb742-69da-4948-b0e2-374a84d3df71/image.png" alt="">
클릭 후 검색으로 GetObject를 검색하고 맨 위에 것을 선택합니다
<img src="https://velog.velcdn.com/images/daun_jung/post/1257edef-a1c1-4fd0-ba87-09b944617d17/image.png" alt="">
그 다음 리소스 추가를 눌러줍니다
<img src="https://velog.velcdn.com/images/daun_jung/post/d29bb535-b7e8-4488-b7ae-44f207d391e2/image.png" alt="">
리소스 유형을 object로 선택 후 {본인 버킷명}/* 으로 설정해 줍니다
(모든 오브젝트에 접근 가능)
<img src="https://velog.velcdn.com/images/daun_jung/post/2560e9df-bc04-4cab-a02f-a33676031a53/image.png" alt="">
Principal을 사진과 같이 수정해 줍니다
<img src="https://velog.velcdn.com/images/daun_jung/post/754026df-6999-41cd-a524-ed264d4803a3/image.png" alt="">
그 다음 변경 사항 저장을 해주세요</p>
<h3 id="3-iam에서-엑세스-키-발급">3. IAM에서 엑세스 키 발급</h3>
<h4 id="iam--액세스-키가-왜-필요할까">IAM &amp; 액세스 키가 왜 필요할까?</h4>
<p>S3는 아무나 접근하면 안되겠죠! 그래서 AWS는 너 누구야? 를 확인합니다</p>
<p>IAM (Identity and Access Management)
: AWS 서비스에 접근할 수 있는 신분증을 만들어주느 곳이에요
Spring 서버가 S3에 업로드하려면 IAM에서 발급한 신분증이 있어야 해요</p>
<p>액세스 키, 시크릿 키
 : IAM에서 발급하는 아이디, 비밀번호 같은 거에요
 Spring boot는 이 키를 가지고 S3에 접근해요</p>
<p> <img src="https://velog.velcdn.com/images/daun_jung/post/2e85b621-15a6-4762-8404-d5ef4fb78e0d/image.png" alt="">
다시 검색창에 iam을 검색해서 들어가 줍니다
<img src="https://velog.velcdn.com/images/daun_jung/post/57ad220e-2843-47e5-b3bd-31e3f48454bb/image.png" alt="">
왼쪽에 액세스 관리 아래 사용자에 들어가서 사용자 생성을 클릭해 줍니다
<img src="https://velog.velcdn.com/images/daun_jung/post/efc8ecbb-c527-42ce-bd16-23888cd449d7/image.png" alt="">
사용자 이름을 likelion-이름 으로 지정하고 다음을 눌러줍니다
<img src="https://velog.velcdn.com/images/daun_jung/post/f0839b8a-7524-40bc-9762-7d49878952ba/image.png" alt="">
직접 정책 연결을 클릭한 뒤 권한 정책의 S3Full을 검색해서 체크한 뒤 다음으로 넘어가 주세요
<img src="https://velog.velcdn.com/images/daun_jung/post/f028814d-7c7a-4c09-94e3-0881dcb7060e/image.png" alt="">
그 다음 건들지 말고 사용자 생성을 해주세요
<img src="https://velog.velcdn.com/images/daun_jung/post/2ff62a83-1fad-4e73-b89a-f143a1360f38/image.png" alt="">
만든 사용자를 클릭한 뒤
<img src="https://velog.velcdn.com/images/daun_jung/post/82466664-8ca2-4451-ac9e-829191443fea/image.png" alt="">
보안 자격 증명에 들어가 액세스 키 만들기를 클릭해 줍니다
<img src="https://velog.velcdn.com/images/daun_jung/post/f2418f9d-2ef6-40bc-8928-087f997f7ca0/image.png" alt="">
AWS 외부에서 실행되는 애플리케이션을 체크한 뒤 다음을 눌러줍니다
<img src="https://velog.velcdn.com/images/daun_jung/post/676ec6cd-718e-4b80-a7cd-3bbe4e7d80be/image.png" alt="">
다음은 건들지 말고 액세스 키 만들기를 클릭해 줍니다
<img src="https://velog.velcdn.com/images/daun_jung/post/ba4e01e3-4e4f-41c9-9392-ab389527f64f/image.png" alt="">
그럼 액세스 키와 비밀 액세스 키가 나오는데 꼭꼭 메모장에 잘 저장해주세요~!!</p>
<h3 id="4-실제-코드-연동">4. 실제 코드 연동</h3>
<p><a href="https://www.notion.so/S3-33644d828e678077bed2ca05b02832e7?source=copy_link">(추가된 코드를 구별하기 어려우신 경우는 이 링크에서 보시면 됩니다)</a></p>
<h4 id="buildgrdle">Build.Grdle</h4>
<pre><code class="language-java">plugins {
    id &#39;java&#39;
    id &#39;org.springframework.boot&#39; version &#39;3.5.13&#39;
    id &#39;io.spring.dependency-management&#39; version &#39;1.1.7&#39;
}

group = &#39;com.likelion&#39;
version = &#39;0.0.1-SNAPSHOT&#39;
description = &#39;likelion-crud&#39;

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-data-jpa&#39;
    implementation &#39;org.springframework.boot:spring-boot-starter-web&#39;
    compileOnly &#39;org.projectlombok:lombok&#39;
    developmentOnly &#39;org.springframework.boot:spring-boot-devtools&#39;
    runtimeOnly &#39;com.mysql:mysql-connector-j&#39;
    annotationProcessor &#39;org.projectlombok:lombok&#39;
    testImplementation &#39;org.springframework.boot:spring-boot-starter-test&#39;
    testRuntimeOnly &#39;org.junit.platform:junit-platform-launcher&#39;
    implementation &#39;org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE&#39;

    //swagger세팅
    implementation &#39;org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.12&#39;
}

dependencyManagement {
    imports {
        mavenBom &quot;org.springframework.cloud:spring-cloud-dependencies:2021.0.8&quot;
    }
}

tasks.named(&#39;test&#39;) {
    useJUnitPlatform()
}

</code></pre>
<p>(dependencyManagement를 선언하면 dependencies 블록에서 버전을 따로 안 써도 BOM이 알아서 맞는 버전을 골라줌)</p>
<h4 id="yml-파일-설정">yml 파일 설정</h4>
<pre><code class="language-java">spring:
  datasource:
    url: jdbc:mysql://localhost:3306/likelion   # 연결할 MySQL 데이터베이스의 주소
    username: root                                   # 본인 MySQL 사용자 이름
    password: mysql0!!                                  # 본인 MySQL 비밀번호
    driver-class-name: com.mysql.cj.jdbc.Driver # MySQL 드라이버 클래스

  jpa:
    hibernate:
      ddl-auto: create     # 테이블 자동 생성 (create, update, validate, none 중 택 1)
    show-sql: true         # 실행되는 SQL을 콘솔에 출력
    properties:
      hibernate:
        format_sql: true   # SQL 쿼리를 보기 좋게 출력 (정렬됨)

logging:
  level:
    org.hibernate.SQL: debug                  # 실행되는 SQL 로그 출력
    org.hibernate.type.descriptor.sql: trace  # 바인딩된 파라미터 값 로그 출력

cloud:
  aws:
    credentials:
      access-key: ${AWS_ACCESS_KEY}
      secret-key: ${AWS_SECRET_KEY}
    s3:
      bucket: ${S3_BUCKET_NAME}
    region:
      static: ap-northeast-2             # 서울 리전
    stack:
      auto: false                        # CloudFormation 스택 자동감지 비활성화

</code></pre>
<p>아까 발급받은 키들과 버킷명을 작성해 주세요</p>
<h4 id="3-s3config-클래스">3. S3Config 클래스</h4>
<table>
<thead>
<tr>
<th>application.yml</th>
<th>S3Config</th>
<th>S3Uploader</th>
</tr>
</thead>
<tbody><tr>
<td>access-key: ...</td>
<td>AmazonS3 빈 생성</td>
<td>amazonS3.putObject()</td>
</tr>
<tr>
<td>secret-key: ...</td>
<td>(인증 정보 주입)</td>
<td>(실제 업로드)</td>
</tr>
<tr>
<td>region: ...</td>
<td></td>
<td></td>
</tr>
<tr>
<td>1. access-key, secret-key로 AWS에 인증</td>
<td></td>
<td></td>
</tr>
<tr>
<td>2. 인증된 S3 클라이언트 객체 생성</td>
<td></td>
<td></td>
</tr>
<tr>
<td>3. 이후엔 어디서든 이 객체 꺼내서 업로드/삭제 가능</td>
<td></td>
<td></td>
</tr>
<tr>
<td>```java</td>
<td></td>
<td></td>
</tr>
<tr>
<td>package com.likelion.likelioncrud.common.config;</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p>import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;</p>
<p>@Configuration          // &quot;나는 설정 클래스야&quot; → Spring이 앱 시작 시 가장 먼저 읽음
public class S3Config {</p>
<pre><code>@Value(&quot;${cloud.aws.credentials.access-key}&quot;)
private String accessKey;   // application.yml에서 값 꺼내서 여기에 넣어줌

@Value(&quot;${cloud.aws.credentials.secret-key}&quot;)
private String secretKey;

@Value(&quot;${cloud.aws.region.static}&quot;)
private String region;      // 실제 값: ap-northeast-2

@Bean                       // &quot;이 메서드가 반환하는 객체를 Spring이 관리해줘&quot;
public AmazonS3 amazonS3() {

    // 1단계: 아이디/비밀번호 묶기
    AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

    // 2단계: 인증 정보 + 리전으로 S3 클라이언트 생성
    return AmazonS3ClientBuilder.standard()
            .withCredentials(new AWSStaticCredentialsProvider(credentials))
            .withRegion(region)
            .build();       // 완성된 S3 클라이언트 반환
}</code></pre><p>}</p>
<pre><code>#### **4. S3Uploader 서비스 클래스**

S3Uploader.upload() 호출
↓
① 파일명 생성 (UUID + 원본파일명)
② 메타데이터 생성 (파일크기, 타입)
③ S3에 업로드
④ 업로드된 URL 반환
↓
사용자에게 URL 응답

UUID : 세상에서 단 하나뿐인 랜덤 문자열을 만들어주는 도구

// UUID 없을 때
홍길동 → &quot;고양이.jpg&quot; 업로드
김철수 → &quot;고양이.jpg&quot; 업로드  ← 홍길동 파일 덮어써짐!

// UUID 있을 때
홍길동 → &quot;a1b2c3_고양이.jpg&quot;
김철수 → &quot;d4e5f6_고양이.jpg&quot;  ← 둘 다 안전하게 저장
```java
package com.likelion.likelioncrud.image;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.UUID;

@Service
@RequiredArgsConstructor    // final 필드 생성자 자동 생성 (Lombok)
public class S3Uploader {

    private final AmazonS3 amazonS3;  // S3Config에서 만든 빈 자동 주입

    @Value(&quot;${cloud.aws.s3.bucket}&quot;)
    private String bucket;            // application.yml에서 버킷명 주입

    public String upload(MultipartFile file) throws IOException {

        // ① 중복 방지 파일명 생성
        String fileName = UUID.randomUUID() + &quot;_&quot; + file.getOriginalFilename();

        // ② S3에 전달할 파일 정보 세팅
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(file.getSize());
        metadata.setContentType(file.getContentType());

        // ③ 실제 업로드
        amazonS3.putObject(bucket, fileName, file.getInputStream(), metadata);

        // ④ 업로드된 파일 URL 반환
        return amazonS3.getUrl(bucket, fileName).toString();
    }
}</code></pre><h4 id="5-errorcode">5. ErrorCode</h4>
<pre><code class="language-java">package com.likelion.likelioncrud.common.response.code;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter // getter 메소드 자동 생성 lombok 어노테이션
@AllArgsConstructor(access = AccessLevel.PRIVATE) // 모든 필드를 파라미터로 받는 생성자 자동 생성 어노테이션
public enum ErrorCode {

    /**
     * 404 NOT FOUND (찾을 수 없음)
     */
    MEMBER_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, &quot;해당 사용자가 없습니다. memberId = &quot;),
    POST_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, &quot;해당 게시글이 없습니다. postId = &quot;),

    /**
     * 400 BAD REQUEST
     */
    VALIDATION_EXCEPTION(HttpStatus.BAD_REQUEST, &quot;유효성 검사에 실패하였습니다 - &quot;),

    /**
     * 500 INTERNAL SERVER ERROR (내부 서버 에러)
     */
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, &quot;내부 서버 에러가 발생했습니다&quot;),

    // S3 이미지 업로드 에러
    FILE_UPLOAD_FAIL_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, &quot;파일 업로드에 실패했습니다.&quot;);

    private final HttpStatus httpStatus;    // HTTP 상태 코드를 스프링에서 쉽게 작성하기 위한 enum값들의 모임
    private final String message;           // 에러 메세지


    public int getHttpStatusCode() {        // HTTP 상태 코드에서 404와 같은 숫자 값만 반환해 주기 위한 메소드
        return httpStatus.value();
    }
}
</code></pre>
<h4 id="6-post">6. Post</h4>
<pre><code class="language-java">package com.likelion.likelioncrud.post.domain;

import com.likelion.likelioncrud.member.domain.Member;
import com.likelion.likelioncrud.post.api.dto.request.PostUpdateRequestDto;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;post_id&quot;)
    private Long postId;

    private String title;

    private String contents;

        // S3에 업로드된 이미지의 URL을 저장, 이미지가 없을 경우 null 허용
    private String imageUrl;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;member_id&quot;)
    private Member member;

    @Builder
    private Post(String title, String contents, String imageUrl, Member member) {
        this.title = title;
        this.contents = contents;
        this.imageUrl = imageUrl; // 이미지 없이 글만 작성할 경우 null로 저장됨
        this.member = member;
    }

    public void update(PostUpdateRequestDto postUpdateRequestDto) {
        this.title = postUpdateRequestDto.title();
        this.contents = postUpdateRequestDto.contents();
    }
}</code></pre>
<h4 id="7-postinforesponsedto">7. PostInfoResponseDto</h4>
<pre><code class="language-java">package com.likelion.likelioncrud.post.api.dto.response;

import com.likelion.likelioncrud.post.domain.Post;
import lombok.Builder;

@Builder
public record PostInfoResponseDto(
        String title,
        String contents,
        String writer,
        String imageUrl // S3에 업로드된 이미지 URL, 이미지 없으면 null 반환
) {
    public static PostInfoResponseDto from(Post post) {
        return PostInfoResponseDto.builder()
                .title(post.getTitle())
                .contents(post.getContents())
                .writer(post.getMember().getName())
                .imageUrl(post.getImageUrl()) // 이미지 없이 저장된 게시글이면 null
                .build();
    }
}</code></pre>
<h4 id="8-postservice">8. PostService</h4>
<p>게시글 텍스트만 보낼 때는 아래와 같은 JSON으로 충분합니다</p>
<pre><code class="language-java">{
  &quot;title&quot;: &quot;제목&quot;,
  &quot;contents&quot;: &quot;내용&quot;
}</code></pre>
<p>근데 파일을 보내려면? 파일은 텍스트가 아니라 바이트데이터(2진수의 나열)이라 JSON에 담을 수 없어요</p>
<p>그래서 multipart/form-data를 사용합니다(여러 데이터를 경계선으로 구분해서 한 요청에 같이 보내는 방식)</p>
<pre><code class="language-java">--boundary
Content-Disposition: form-data; name=&quot;data&quot;
{ &quot;title&quot;: &quot;제목&quot;, &quot;contents&quot;: &quot;내용&quot; }

--boundary
Content-Disposition: form-data; name=&quot;image&quot;; filename=&quot;고양이.jpg&quot;
Content-Type: image/jpeg
(이미지 바이트 데이터.....)
--boundary--</code></pre>
<p>MultipartFile이란 : </p>
<p>HTTP로 전송된 파일 데이터를 Spring이 자바 객체로 변환해서 우리가 쓸 수 있게 해주는 인터페이스입니다</p>
<pre><code class="language-java">package com.likelion.likelioncrud.post.application;

import com.likelion.likelioncrud.common.exception.BusinessException;
import com.likelion.likelioncrud.common.response.code.ErrorCode;
import com.likelion.likelioncrud.image.S3Uploader;
import com.likelion.likelioncrud.member.domain.Member;
import com.likelion.likelioncrud.member.domain.repository.MemberRepository;
import com.likelion.likelioncrud.post.api.dto.request.PostSaveRequestDto;
import com.likelion.likelioncrud.post.api.dto.request.PostUpdateRequestDto;
import com.likelion.likelioncrud.post.api.dto.response.PostInfoResponseDto;
import com.likelion.likelioncrud.post.domain.Post;
import com.likelion.likelioncrud.post.domain.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PostService {

    private final MemberRepository memberRepository;
    private final PostRepository postRepository;
    private final S3Uploader s3Uploader; // S3 이미지 업로드를 위해 주입

    // 게시물 저장
    @Transactional
    public void postSave(PostSaveRequestDto postSaveRequestDto, MultipartFile image) {
        Member member = memberRepository.findById(postSaveRequestDto.memberId()).orElseThrow(() -&gt; new BusinessException(ErrorCode.MEMBER_NOT_FOUND_EXCEPTION, ErrorCode.MEMBER_NOT_FOUND_EXCEPTION.getMessage() + postSaveRequestDto.memberId()));

        // 이미지가 있을 때만 S3에 업로드. 이미지 없이 글만 작성하는 경우도 허용하기 위해 null 체크
        String imageUrl = null;
        if (image != null &amp;&amp; !image.isEmpty()) {
            try {
                imageUrl = s3Uploader.upload(image); // S3 업로드 후 반환된 URL을 저장
            } catch (IOException e) {
                // 업로드 실패 시 커스텀 예외로 변환해서 던짐 → GlobalExceptionHandler가 처리
                throw new BusinessException(ErrorCode.FILE_UPLOAD_FAIL_EXCEPTION, ErrorCode.FILE_UPLOAD_FAIL_EXCEPTION.getMessage());
            }
        }

        Post post = Post.builder()
                .title(postSaveRequestDto.title())
                .contents(postSaveRequestDto.contents())
                .imageUrl(imageUrl) // 이미지 없으면 null, 있으면 S3 URL
                .member(member)
                .build();

        postRepository.save(post);
    }

    // 특정 작성자가 작성한 게시글 목록을 조회
    public Page&lt;PostInfoResponseDto&gt; postFindMember(Long memberId, Pageable pageable) {
        Member member = memberRepository.findById(memberId).orElseThrow(() -&gt; new BusinessException(ErrorCode.MEMBER_NOT_FOUND_EXCEPTION, ErrorCode.MEMBER_NOT_FOUND_EXCEPTION.getMessage() + memberId));

        Page&lt;Post&gt; posts = postRepository.findByMember(member, pageable);
        return posts.map(PostInfoResponseDto::from);
    }

    // 게시물 수정
    @Transactional
    public void postUpdate(Long postId, PostUpdateRequestDto postUpdateRequestDto)
    {
        Post post = postRepository.findById(postId).orElseThrow(() -&gt; new BusinessException(ErrorCode.POST_NOT_FOUND_EXCEPTION, ErrorCode.POST_NOT_FOUND_EXCEPTION.getMessage() + postId));
        post.update(postUpdateRequestDto);
    }

    // 게시물 삭제
    @Transactional
    public void postDelete(Long postId) {
        Post post = postRepository.findById(postId).orElseThrow(() -&gt; new BusinessException(ErrorCode.POST_NOT_FOUND_EXCEPTION, ErrorCode.POST_NOT_FOUND_EXCEPTION.getMessage() + postId));
        postRepository.delete(post);
    }
}</code></pre>
<h4 id="9-postcontroller">9. PostController</h4>
<pre><code class="language-java">package com.likelion.likelioncrud.post.api;

import com.likelion.likelioncrud.common.response.code.SuccessCode;
import com.likelion.likelioncrud.common.template.ApiResTemplate;
import com.likelion.likelioncrud.post.api.dto.request.PostSaveRequestDto;
import com.likelion.likelioncrud.post.api.dto.request.PostUpdateRequestDto;
import com.likelion.likelioncrud.post.api.dto.response.PostInfoResponseDto;
import com.likelion.likelioncrud.post.application.PostService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/post&quot;)
@Tag(name = &quot;POST API&quot;, description = &quot;게시글 관리하는 api &quot;)
public class PostController {

    private final PostService postService;

    // 게시물 저장
    // consumes = &quot;multipart/form-data&quot;: JSON이 아닌 파일 전송 방식으로 요청을 받음
    // @RequestPart(&quot;data&quot;): multipart 요청에서 &quot;data&quot; 키에 담긴 JSON을 DTO로 변환
    // required = false: 이미지 없이 글만 저장하는 경우도 허용
    @PostMapping(consumes = &quot;multipart/form-data&quot;)
    @Operation(summary = &quot;게시물 저장&quot;, description = &quot;게시물 저장. 이미지는 선택사항입니다.&quot;)
    public ApiResTemplate&lt;Void&gt; postSave(
            @RequestPart(&quot;data&quot;) PostSaveRequestDto postSaveRequestDto,
            @RequestPart(value = &quot;image&quot;, required = false) MultipartFile image) {
        postService.postSave(postSaveRequestDto, image);
        return ApiResTemplate.successWithNoContent(SuccessCode.POST_SAVE_SUCCESS);
    }

    // 사용자 id를 기준으로 해당 사용자가 작성한 게시글 목록 조회
    @GetMapping(&quot;/{memberId}&quot;)
    @Operation(summary = &quot;게시물 memberId로 조회&quot;, description = &quot;게시물 memberId로 조회&quot;)
    public ApiResTemplate&lt;Page&lt;PostInfoResponseDto&gt;&gt; myPostFindAll(@PathVariable(&quot;memberId&quot;) Long memberId, @ParameterObject @PageableDefault(size = 10, sort = &quot;postId&quot;, direction = Sort.Direction.ASC) Pageable pageable) {
        Page&lt;PostInfoResponseDto&gt; posts = postService.postFindMember(memberId, pageable);
        return ApiResTemplate.successResponse(SuccessCode.GET_SUCCESS, posts);
    }

    // 게시물 id를 기준으로 사용자가 작성한 게시물 수정
    @PatchMapping(&quot;/{postId}&quot;)
    @Operation(summary = &quot;게시물 Id로 수정&quot;, description = &quot;게시물 제목, 내용 수정&quot;)
    public ApiResTemplate&lt;Void&gt; postUpdate(@PathVariable(&quot;postId&quot;) Long postId,
                                             @RequestBody PostUpdateRequestDto postUpdateRequestDto) {
        postService.postUpdate(postId, postUpdateRequestDto);
        return ApiResTemplate.successWithNoContent(SuccessCode.POST_UPDATE_SUCCESS);
    }

    // 게시물 id를 기준으로 사용자가 작성한 게시물 삭제
    @DeleteMapping(&quot;/{postId}&quot;)
    @Operation(summary = &quot;게시물 삭제&quot;, description = &quot;게시물 Id로 삭제&quot;)
    public ApiResTemplate&lt;Void&gt; postDelete(@PathVariable(&quot;postId&quot;) Long postId) {
        postService.postDelete(postId);
        return ApiResTemplate.successWithNoContent(SuccessCode.POST_DELETE_SUCCESS);
    }
}
</code></pre>
<h4 id="10-테스트">10. 테스트</h4>
<p>스웨거가 아니라 Postman으로 테스트하는 이유는 뭔가요?</p>
<table>
<thead>
<tr>
<th><strong>비교 항목</strong></th>
<th><strong>Swagger</strong></th>
<th><strong>Postman</strong></th>
</tr>
</thead>
<tbody><tr>
<td>multipart Content-Type</td>
<td>자동으로 <code>octet-stream</code>으로 보내버림</td>
<td><code>multipart/form-data</code>를 안정적으로 처리함</td>
</tr>
<tr>
<td>헤더 제어</td>
<td>제한적</td>
<td>자유롭게 설정 가능</td>
</tr>
<tr>
<td>용도</td>
<td>API 문서 확인용</td>
<td>실제 테스트용</td>
</tr>
</tbody></table>
<p><code>octet</code> = 8비트(1바이트). 즉 <code>application/octet-stream</code> = &quot;그냥 바이트 덩어리인데 타입을 모르겠음&quot; 이라는 의미.
<img src="https://velog.velcdn.com/images/daun_jung/post/165b0e70-0731-449a-b76e-96d51e3cdc27/image.png" alt="">
포스트맨을 켜서 POST로 위와 같이 새로 생성하고
<img src="https://velog.velcdn.com/images/daun_jung/post/b8b0ec48-73da-4de6-bacd-9b8dff10c4b2/image.png" alt="">
body는 form-data로 설정해주세요
<img src="https://velog.velcdn.com/images/daun_jung/post/e913cdfc-ea22-4b2f-a18f-9f807c1b9bde/image.png" alt="">
그 다음 맨 오른쪽 쩜쩜쩜을 클릭해 Content-Type을 추가해줍니다 
<img src="https://velog.velcdn.com/images/daun_jung/post/d04319e4-73be-42ca-93b7-8a38e8f7b7a6/image.png" alt="">
그 후 data키는 Text 타입으로 사진과 같이 만들어 주시고
(꼭 Content-Type안에 application/json를 넣어주세요!)
<img src="https://velog.velcdn.com/images/daun_jung/post/113e2a40-2fb0-44da-b1cf-07b12aeae614/image.png" alt="">
image키는 file 타입으로 만들어서 
<img src="https://velog.velcdn.com/images/daun_jung/post/c915212c-a2d5-4997-82ae-f93ca42b7495/image.png" alt="">
이렇게 이미지를 넣어줍니다
<img src="https://velog.velcdn.com/images/daun_jung/post/79d998b6-fee9-4fd6-819c-30465a50e1ad/image.png" alt="">
성공했다면
<img src="https://velog.velcdn.com/images/daun_jung/post/a5f0b620-3e0e-4fea-a2b2-7da6d1d82518/image.png" alt="">
S3 버킷에 들어가서 
<img src="https://velog.velcdn.com/images/daun_jung/post/a75cb335-da9d-45fb-830c-9a5d593de430/image.png" alt="">
업로드된 객체에 들어가서
<img src="https://velog.velcdn.com/images/daun_jung/post/f2289744-7559-4a4d-9c95-e1915b77e812/image.png" alt="">
속성 속 객체 URL을 클릭해보면 
<img src="https://velog.velcdn.com/images/daun_jung/post/a4fcd3ab-57dc-4bfe-a77a-dfadf88ad04f/image.png" alt="">
사진이 뜨면 성공입니다~
<img src="https://velog.velcdn.com/images/daun_jung/post/da1f6fc2-3efe-490f-aafc-d7f2bcb1fd5b/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OAuth 완벽 이해하기]]></title>
            <link>https://velog.io/@daun_jung/OAuth-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@daun_jung/OAuth-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 10 Jul 2025 14:07:40 GMT</pubDate>
            <description><![CDATA[<p>멋쟁이 사자처럼 13기 강의를 진행하며 만든 자료를 블로그에 공유합니다.</p>
<h1 id="oauthopenid-authentication란">OAuth(OpenID Authentication)란?</h1>
<p>OAuth는 인증(Authentication)과 권한(Authorization) 부여를 위한 개방형 표준 프로토콜입니다!</p>
<p>사용자가 자신의 <strong>개인 정보를 직접 제공하지 않고</strong>, 다양한 외부 플랫폼(예: 구글, 깃허브, 네이버 등)의 서비스에 안전하게 접근할 수 있도록 돕습니다!</p>
<hr>
<h3 id="왜-필요할까요">왜 필요할까요~?</h3>
<ul>
<li><p>문제점</p>
<p>  OAuth 이전에는 다른 애플리케이션에 로그인하거나 개인 정보를 제공할 때 사용자 이름과 비밀번호 등을 직접 입력하여 제공해야 했습니다!</p>
<p>  이런 방식은 보안 위험이 크며, 사용자 계정 정보가 노출될 수 있는 구조입니다!</p>
</li>
<li><p>해결 방식</p>
<p>  하지만 OAuth는 사용자가 플랫폼에서 직접 로그인하고, 애플리케이션은 토큰(token)을 통해 일부 권한만 위임받아 사용합니다!</p>
<p>  이 토큰은 사용자가 승인한 범위 내에서만 사용되며, 아이디와 비밀번호 노출 없이 안전하게 인증과 인가 처리를 할 수 있습니다!</p>
</li>
</ul>
<hr>
<h3 id="용어-정리">용어 정리</h3>
<p>클라이언트(Client) : 리소스에 접근하는 애플리케이션 혹은 서비스 (Spring boot 서버)</p>
<p>인증 서버(Authorization Server) : 인증, 인가를 수행하는 서버로 access token을 발급</p>
<p>리소스 서버(Resource Server) : 구글, 깃허브, 카카오, 네이버 등 사용자의 리소스를 가지고 있는 서버를 의미</p>
<p>인가 코드(Authorization Code) : 사용자가 로그인 성공 후 발급받는 코드로 access token 발급 시 필요</p>
<hr>
<h3 id="구글-소셜-로그인-흐름">구글 소셜 로그인 흐름</h3>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/dd733919-c1cd-4c6d-886c-679bdadfe036/image.png" alt=""></p>
<ol>
<li>구글 로그인 화면으로 이동 </li>
<li>로그인 성공 → 코드 발급  : 사용자가 로그인에 성공하고 권한을 허용하면 구글은 인가 코드(Authorization Code)를 만들어서 서버주소(redirect URI)로 보내줌</li>
<li>인가 코드를 서버로 전달 : 이 코드는 주소창에 <code>?code=abcd1234</code> 처럼 붙어서 서버로 전달됨</li>
<li>서버가 구글에 Access Token 요청 : 서버는 받은 code를 들고 구글에 인증해달라고 요청함</li>
<li>구글이 Access Token발급 : 구글은 요청을 확인 후 Access Token(일종의 열쇠처럼 작동)을 돌려줌</li>
<li>서버가 사용자 정보 요청 : 서버는 이 Access Token을 이용해 구글 사용자 정보 API에 요청</li>
<li>구글이 사용자 정보 전달 : 구글은 요청이 맞다면 <code>&quot;email&quot;: &quot;[abc@gmail.com](mailto:abc@gmail.com)&quot;, &quot;name&quot;: &quot;홍길동&quot;</code> 같은 정보를 JSON으로 넘겨줌</li>
<li>서버가 사용자 DB 저장 및 로그인 처리 : 서버는 이 정보로 이미 가입돼 있으면 로그인 처리, 없으면 새로 회원가입 진행</li>
<li>서버에서 자체 JWT 토큰 생성 → 클라이언트에 응답 : 서버는 사용자의 로그인에 성공하면 자제적인 Access Token(예:JWT)을 만들어 클라리언트에 반환, 이 토큰은 이후부터 로그인 했다고 증명하는 열쇠가 되어 다른 API호출 시 함께 보내면 인증됨</li>
</ol>
<ul>
<li>참고 사진</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/670b43d5-e4c3-4b2d-ae47-f32be41db117/image.png" alt=""></p>
<hr>
<h1 id="google-cloud-에서-서비스-등록">google cloud 에서 서비스 등록</h1>
<p>&lt;구글이 해당 서버가 신뢰할 수 있는 클라이언트인지 사전에 식별하고 인증하기 위해 서비스를 등록합니다!&gt;</p>
<ol>
<li>구글 클라우드 접속</li>
</ol>
<p><a href="https://console.cloud.google.com/welcome?pli=1&amp;inv=1&amp;invt=Abyf9Q&amp;project=gradationk2"></a></p>
<ol>
<li>프로젝트 선택 클릭</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/15ce479f-05c3-4b4e-beca-f1f36ce9e5ad/image.png" alt=""></p>
<ol>
<li>새 프로젝트 만들기</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/48c62d9f-6723-43dd-aa2b-17656e2d6353/image.png" alt=""></p>
<ol>
<li>Likelion13으로 생성</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/7a20655e-004b-4f9f-9e67-2bf134666734/image.png" alt=""></p>
<ol>
<li>API 및 서비스 클릭</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/adbdab15-8dfd-4bc8-a134-15b19f790abf/image.png" alt=""></p>
<ol>
<li>사용자 인증 정보 클릭</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/1eef95d0-3b83-4e41-a68d-31fa2c6b72b1/image.png" alt=""></p>
<ol>
<li>동의 화면 구성 클릭</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/d931b4c8-a493-40ea-8e64-3f571f0a6e5c/image.png" alt=""></p>
<ol>
<li>사진과 같이 입력</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/d91a5067-2751-4431-9d98-1f609a679513/image.png" alt=""></p>
<ol>
<li>외부로 선택해 주세요~</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/4a36de11-1953-4fc5-9636-fe0a66228026/image.png" alt=""></p>
<ol>
<li>사용하시는 이메일을 입력해 주세요~</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/c7d8a904-af0a-4750-9e06-bf1b1d473d27/image.png" alt=""></p>
<ol>
<li>동의 후 만들기를 클릭해 주세요~</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/de79eb37-4d44-4a0d-9c14-f3730de74409/image.png" alt=""></p>
<ol>
<li>데이터 액세스를 클릭 후 범위 추가 또는 삭제를 눌러 주세요~</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/17449536-7e05-415f-83ea-d62f8907dc14/image.png" alt=""></p>
<ol>
<li>기본적으로 많이 사용되는 email, profile를 선택 후 업데이트를 클릭해 주세요~</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/615f6c28-adf1-4931-9b53-c5adf58e42b5/image.png" alt=""></p>
<ol>
<li>아래 사진처럼 뜨면 성공입니다!!</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/683f2a50-b43e-42b8-882c-9d8bc673d38d/image.png" alt=""></p>
<ol>
<li>그 다음 대상으로 들어가 테스트 사용자의 Add users를 클릭해 주세요~</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/5c47a64e-e489-400c-9f97-904eadb8d457/image.png" alt=""></p>
<ol>
<li>그 다음 본인 이메일을 입력해 주시면 됩니다~</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/8e0a0aec-260b-434b-9ed1-74432defdeb3/image.png" alt=""></p>
<ol>
<li>다시 API 및 서비스를 들어가서 사용자 인증정보로 들어가 주세요~</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/a489d52a-5b11-485d-8566-643bf040dbc1/image.png" alt=""></p>
<ol>
<li>사용자 인증 정보 만들기에서 OAuth 클라이언트 ID를 클릭해 주세요~</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/25543e21-3e03-4518-b281-9330ace85c66/image.png" alt=""></p>
<ol>
<li><p>애플리케이션 유형 → 웹 애플리케이션</p>
</li>
</ol>
<p>이름 → LikeLion13</p>
<p>승인된 리디렉션 URI → <a href="http://localhost:8080/login/oauth2/code/google">http://localhost:8080/login/oauth2/code/google</a> 을 정확히 입력해 주세요~ (메모장에 입력해 놓아주세요~)</p>
<p>다음 만들기 버튼까지 눌러주시면 됩니다~</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/8a3694ea-ea20-4627-a965-c477a18b3680/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/4df55547-326c-4a61-815c-e7988f1a814f/image.png" alt=""></p>
<ol>
<li>그러면 클라이언트 ID랑 비밀번호가 뜨는데 이걸 꼭 URI랑 같이 메모장에 복붙해 주세요!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!</li>
</ol>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/7c92a83a-d8d6-4b18-94e6-e771d3fdf1d3/image.png" alt=""></p>
<h1 id="테스트">테스트</h1>
<p><a href="https://accounts.google.com/o/oauth2/v2/auth?client_id=">https://accounts.google.com/o/oauth2/v2/auth?client_id=</a><CLIENT_ID>&amp;redirect_uri=<REDIRECT_URI>&amp;response_type=code&amp;scope=email%20profile</p>
<p>위 링크에 메모장에 적어 놓은 클라이언트 ID와 리디렉션 URI로 변경하여 접속해 볼게요~ </p>
<p>아래 사진과 같은 화면이 나온다면 성공입니다~</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/7e92822b-afd0-4145-8822-f81763749dc0/image.png" alt=""></p>
<h1 id="인가코드-authorization-code-출력해보기">인가코드 (Authorization code) 출력해보기</h1>
<p>우선 인가코드를 발급해보는 테스트를 진행해보겠습니다!</p>
<p>이는 위에서 구글 로그인 창으로 이동해, 로그인이 완료 되었다면 리디렉션 URI에 담겨 돌아오는 authorization_code 값을 눈으로 확인해보는 과정입니다!</p>
<p>토큰 강의에서 사용했던 프로젝트를 열어주세요~</p>
<p>그 다음 build.gradle에서 아래 의존성을 추가해주세요~</p>
<pre><code class="language-java">dependencies {
    ...

    implementation &#39;com.google.code.gson:gson:2.10.1&#39;

    ...
}</code></pre>
<p>추가 후 코끼리 눌러주세요~</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/ed10f53f-2858-401d-90ac-48b47370d3ad/image.png" alt=""></p>
<h3 id="applicationyml">application.yml</h3>
<pre><code class="language-java">spring:
  profiles:
    active: prod

  datasource:
    url: ${spring.datasource.url}
    username: ${spring.datasource.username}
    password: ${spring.datasource.password}
    driver-class-name: ${spring.datasource.driver-class-name}

  jpa:
    database: mysql
    database-platform: org.hibernate.dialect.MySQL8Dialect
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: false
        use_sql_comments: true
        show_sql: true
    open-in-view: false

logging:
  level:
    org.hibernate.SQL: debug
    org.hibernate.type.descriptor.sql: trace

jwt:
  secret: ${JWT_SECRET}
  access-token-validity-in-milliseconds: ${JWT_ACCESS_TOKEN_VALIDITY_IN_MILLISECONDS}
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}</code></pre>
<h3 id="application-prodyml">application-prod.yml</h3>
<p>데이터베이스 스키마 생성해 주세요~</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/d4ac6b40-3245-4657-961e-f485bddf5532/image.png" alt=""></p>
<pre><code class="language-java">spring:
  config:
    activate:
      on-profile: prod

  datasource:
    url: jdbc:mysql://localhost:3306/likelionoauth2
    username: root
    password: #각자 비밀번호로 변경
    driver-class-name: com.mysql.cj.jdbc.Driver

jwt:
  secret: ???????  # openssl rand -base64 64
  access-token-validity-in-milliseconds: 3600000
client-id: 클라이언트 ID
client-secret: 클라이언트 SECRET KEY

token:
  expire:
    time: 1800000</code></pre>
<h3 id="gitignore">.gitignore</h3>
<pre><code class="language-java">src/**/application-prod.yml</code></pre>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/7b09fc0f-1c87-453b-aa39-82ee106fd10f/image.png" alt="">
<img src="attachment:f9ee34f0-c553-4db5-8cd1-b7cad8a042c6:image.png" alt="image.png"></p>
<h3 id="authlogincontroller">AuthLoginController</h3>
<p>아래 코드는 OAuth2로그인 흐름을 테스트하기 위해 작성한 최소 구성의 컨트롤러입니다!</p>
<p>구글에서 받은 인가코드를 서버가 받아 AuthLoginServer에게 전달합니다!</p>
<pre><code class="language-java">package com.likelion.likelionjwt.oauth.api;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/login/oauth2&quot;)
public class AuthLoginController {
    private final AuthLoginService authLoginService;

    @GetMapping(&quot;/code/{registrationID}&quot;)
    public void googleLogin(@RequestParam String code, @PathVariable String registrationID){
        authLoginService.socialLogin(code, registrationID);
    }
}</code></pre>
<ul>
<li>code : 인증 서버에서 받은 authorization code</li>
<li>registrationId : 사용자가 로그인한 소셜 로그인의 id</li>
<li>예를들어 <a href="http://localhost:8080/login/oauth2/code/google?code=abc123"><code>http://localhost:8080/login/oauth2/code/google?code=abc123</code></a> 라는 URL이 code : abc123, registrationId : google이 됩니다!</li>
</ul>
<h3 id="authloginservice">AuthLoginService</h3>
<p>이 코드도 테스트를 위한 코드로 실제로 토큰을 요청하거나 사용자 정보를 가져오지 않고 단순히 인가 코드와 소셜로그인의 id(즉 google)을 출력하는 구조입니다!</p>
<pre><code class="language-java">package com.likelion.likelionjwt.oauth.application;

import org.springframework.stereotype.Service;

@Service
public class AuthLoginService {

    public void socialLogin(String code, String registrationId){
        System.out.println(&quot;code = &quot; + code);
        System.out.println(&quot;registrationId = &quot; + registrationId);
    }
}</code></pre>
<ul>
<li>컨트롤러에서 받은 authorization code와 소셜 로그인 id를 받아 콘솔에 출력</li>
</ul>
<h3 id="securityconfig">SecurityConfig</h3>
<p>저번 토큰 강의에서 작성했던 코드에 이어서 작성할게요~</p>
<pre><code class="language-java">package com.likelion.likelionjwt.global.config;

import com.likelion.likelionjwt.global.jwt.JwtAuthorizationFilter;
import lombok.RequiredArgsConstructor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration  // 하나 이상의 @Bean 메서드를 가지고 있어 spring 컨테이너가 해당 메서드들을 관리하고 빈으로 등록
@EnableWebSecurity  // spring security 활성화, 웹 보안 설정 제공
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtAuthorizationFilter jwtAuthorizationFilter;

    @Bean // 보안 필터 체인을 정의하는 Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .httpBasic(AbstractHttpConfigurer::disable) // 기본 HTTP 인증 비활성화
                .formLogin(AbstractHttpConfigurer::disable) // 스프링 기본 폼 로그인 비활성화
                .sessionManagement(sessionManagement -&gt; sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 세션 정책을 stateless로 설정 -&gt; 서버가 세션을 생성하지 않고 토큰 기반 인증을 사용하도록 설정 = jwt 방식
                .csrf(AbstractHttpConfigurer::disable)  // csrf 비활성화
                .authorizeHttpRequests(authorizeRequests -&gt; authorizeRequests
                                .requestMatchers(&quot;/login/oauth2/**&quot;).permitAll()
                        .requestMatchers(&quot;/members/**&quot;).permitAll() // members로 시작하는 경로는 허용
                        .requestMatchers(&quot;/posts/**&quot;).permitAll() // posts도 허용
                        .anyRequest().authenticated()   // 그 외 모든 요청은 인증 필요
                )
                .addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class)
                // 지정된 필터 앞에 커스텀 필터(jwtAuthorizationFilter) 추가
                .formLogin(AbstractHttpConfigurer::disable)
                .build();
    }
    @Bean
    public PasswordEncoder passwordEncoder() { // 패스워드 암호화
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
   }</code></pre>
<aside>
💡

<p><a href="https://accounts.google.com/o/oauth2/v2/auth?client_id=">https://accounts.google.com/o/oauth2/v2/auth?client_id=</a><CLIENT_ID>&amp;redirect_uri=<REDIRECT_URI>&amp;response_type=code&amp;scope=email%20profile</p>
<p>다시 아까 작성했던 링크로 들어가 로그인 해주세요!!</p>
</aside>

<p><img src="https://velog.velcdn.com/images/daun_jung/post/6469bd69-8c96-4bf6-9e20-336783ebfe13/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/8eec086b-6313-4d1c-9fde-db1695dc600d/image.png" alt=""></p>
<p>빈 화면과 콘솔에 인가코드, 구글이 출력되면 성공입니다~</p>
<h1 id="구글-로그인과-회원가입-토큰-발급">구글 로그인과 회원가입, 토큰 발급</h1>
<p>이제 구글로 로그인할 때 처음 방문한 사용자는 회원가입 후 로그인(토큰 발급), 이미 회원가입이 되어있다면 바로 로그인을 해주도록 만들어 보겠습니당~~</p>
<h2 id="domain">Domain</h2>
<h3 id="member">Member</h3>
<p>지금까지 실습해오던 member 도메인에 이어서 작성해줄게요~</p>
<pre><code class="language-java">package com.likelion.likelionjwt.member.domain;

import com.likelion.likelionjwt.post.domain.Post;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;member_id&quot;)
    private Long memberId;

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

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

    @Column(name = &quot;member_picture_url&quot;)
    private String pictureUrl;

    @Enumerated(EnumType.STRING)
    @Column(name = &quot;member_role&quot;, nullable = false)
    private Role role;

    @OneToMany(mappedBy = &quot;member&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
    private List&lt;Post&gt; posts = new ArrayList&lt;&gt;();

    @Builder
    public Member(String email, String password, String name, String pictureUrl, Role role) {
        this.email = email;
        this.password = password;
        this.name = name;
        this.pictureUrl = pictureUrl;
        this.role = role;
    }

}</code></pre>
<ul>
<li>이메일, 이름, 사용자 권한을 필수값으로 수정해주고</li>
<li>구글 등의 소셜 로그인에서 제공하는 프로필 이미지 주소를 저장하도록 수정해 줄게요~</li>
</ul>
<h2 id="dto">DTO</h2>
<h3 id="token">Token</h3>
<p>구글로부터 받은 access_token값을 받아서 자바 객체로 변환하거나 서버에서 JWT를 만들어 클라이언트에서 반환할 때 사용하는 DTO 입니다~</p>
<pre><code class="language-java">package com.likelion.likelionjwt.oauth.api.dto;

import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
@AllArgsConstructor
public class Token {

    @SerializedName(&quot;access_token&quot;)
    private String accessToken;
}</code></pre>
<ul>
<li>SerializedName은 직렬화, 역직렬화 시 이름 매핑을 해줍니다!</li>
<li>구글이 반환하는 JSON에서 access_token이라는 키를 이 클래스의 accessToken필드에 매핑되도록 지정합니다</li>
</ul>
<h3 id="userinfo">UserInfo</h3>
<p>MemberInfoResDto가 있는데 왜 또 만드는지 의문이 드시는 분들이 계실 수도 있을 것 같아용!</p>
<p>둘의 차이점은 MemberInfoResDto는 로그인 성공 후 클라이언트에 전달할 정보를 담고 있고, UserInfo는 구글 API로부터 응답받은 JSON을 파싱하는 역할을 합니다!!</p>
<p>즉, UserInfo는 구글로부터 받은 사용자 이름, 이메일, 프로필 사진 등을 담습니다!</p>
<pre><code class="language-java">package com.likelion.likelionjwt.oauth.api.dto;

import com.google.gson.annotations.SerializedName;
import lombok.Data;

@Data
public class UserInfo {
    private String id;

    private String email;

    @SerializedName(&quot;verified_email&quot;)
    private Boolean verifiedEmail;

    private String name;

    @SerializedName(&quot;given_name&quot;)
    private String givenName;

    @SerializedName(&quot;family_name&quot;)
    private String familyName;

    @SerializedName(&quot;picture&quot;)
    private String pictureUrl;

    private String locale;
}
</code></pre>
<p>Google OAuth2 로그인 후 사용자 정보는 아래와 같은 형태의 JSON으로 응답됩니다~</p>
<pre><code class="language-java">{
  &quot;id&quot;: &quot;112345678901234567890&quot;,
  &quot;email&quot;: &quot;user@gmail.com&quot;,
  &quot;verified_email&quot;: true,
  &quot;name&quot;: &quot;홍길동&quot;,
  &quot;given_name&quot;: &quot;길동&quot;,
  &quot;family_name&quot;: &quot;홍&quot;,
  &quot;picture&quot;: &quot;https://lh3.googleusercontent.com/a-/AOh14GgNEXAMPLE&quot;,
  &quot;locale&quot;: &quot;ko&quot;
}</code></pre>
<h1 id="repository">repository</h1>
<p>MemberRepository는 토큰 강의에서 만들었기에 그대로 갑니다~</p>
<p>이는 사용자가 이미 가입된 회원인지 확인하거나 새로 가입시키기 위해 사용됩니다!</p>
<ul>
<li><p>MemberRepository</p>
<pre><code class="language-java">  package com.likelion.likelionjwt.member.domain.repository;

  import com.likelion.likelionjwt.member.domain.Member;
  import org.springframework.data.jpa.repository.JpaRepository;

  import java.util.Optional;

  public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
      Optional&lt;Member&gt; findByEmail(String email);
      boolean existsByEmail(String email);
  }</code></pre>
</li>
</ul>
<h2 id="global">global</h2>
<h3 id="securityconfig-1">SecurityConfig</h3>
<p>작성하던 코드에 이어서 작성해 주세요~</p>
<p>이 클래스는 spring boot 애플리케이션에서 폼로그인 없이 JWT만으로 인증 처리가 가능하도록 만들어 줍니다!</p>
<pre><code class="language-java">package com.likelion.likelionjwt.global.config;

import com.likelion.likelionjwt.global.jwt.JwtAuthorizationFilter;
import com.likelion.likelionjwt.global.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration  // 하나 이상의 @Bean 메서드를 가지고 있어 spring 컨테이너가 해당 메서드들을 관리하고 빈으로 등록
@EnableWebSecurity  // spring security 활성화, 웹 보안 설정 제공
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtAuthorizationFilter jwtAuthorizationFilter;
    private final JwtTokenProvider jwtTokenProvider;

    @Bean // 보안 필터 체인을 정의하는 Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .httpBasic(AbstractHttpConfigurer::disable) // 기본 HTTP 인증 비활성화
                .formLogin(AbstractHttpConfigurer::disable) // 스프링 기본 폼 로그인 비활성화
                .sessionManagement(sessionManagement -&gt; sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 세션 정책을 stateless로 설정 -&gt; 서버가 세션을 생성하지 않고 토큰 기반 인증을 사용하도록 설정 = jwt 방식
                .csrf(AbstractHttpConfigurer::disable)  // csrf 비활성화
                .sessionManagement(sessionManagement -&gt; sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(authorizeRequests -&gt; authorizeRequests
                        .requestMatchers(&quot;/login/oauth2/**&quot;).permitAll()
                        .requestMatchers(&quot;/members/**&quot;).permitAll() // members로 시작하는 경로는 허용
                        .requestMatchers(&quot;/posts/**&quot;).permitAll() // posts도 허용
                        .anyRequest().authenticated()   // 그 외 모든 요청은 인증 필요
                )
                .addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class)
                // 지정된 필터 앞에 커스텀 필터(jwtAuthorizationFilter) 추가
                .formLogin(AbstractHttpConfigurer::disable)
                .addFilterBefore(new JwtAuthorizationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class)
                .build();
    }
    @Bean
    public PasswordEncoder passwordEncoder() { // 패스워드 암호화
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}</code></pre>
<p>추가 후 </p>
<p><code>Cannot resolve symbol &#39;TokenProvider’</code></p>
<p><code>Could not autowire. No beans of &#39;TokenProvider&#39; type found.</code></p>
<p><code>Cannot resolve symbol &#39;JwtFilter’</code></p>
<p>위와 같은 에러가 뜨는 건 당연하답니다~</p>
<h3 id="tokenprovider">TokenProvider</h3>
<p>이 코드도 작성하던 곳에 이어서 작성해 주세요~</p>
<p>이 클래스는 OAuth2 로그인 후 사용자 정보를 바탕으로 JWT를 생성하고 클라이언트가 보낸 JWT를 검증해 인증 객체를 만들어줍니다!</p>
<pre><code class="language-java">package com.likelion.likelionjwt.global.jwt;

import com.likelion.likelionjwt.common.error.ErrorCode;
import com.likelion.likelionjwt.common.exception.BusinessException;
import com.likelion.likelionjwt.member.domain.Member;
import com.likelion.likelionjwt.member.domain.repository.MemberRepository;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.crypto.SecretKey;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j  // 로그 작성
@Component  // spring bean에 등록
public class JwtTokenProvider {

    private static final String AUTHORITIES_KEY = &quot;auth&quot;; // 권한 키 상수 추가 
    private final MemberRepository memberRepository;

    @Value(&quot;${token.expire.time}&quot;) // factory annotation 임포트
    private String tokenExpireTime; // 토큰 만료 시간

    @Value(&quot;${jwt.secret}&quot;)
    private String secret;  // 비밀키

    private SecretKey key;  // 객체 key

    public JwtTokenProvider(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @PostConstruct  // Bean이 초기화 된 후에 실행
    public void init() {    // 필터 객체를 초기화하고 서비스에 추가하기 위한 메소드 init
        byte[] keyBytes = Decoders.BASE64.decode(secret);   // 시크릿 키 디코딩 후
        this.key = Keys.hmacShaKeyFor(keyBytes); // 키 암호화
    }

    // 토큰 생성
    public String generateToken(Member member) {
        // 만료 시간 설정 util로 임포트
        Date now = new Date();
        Date expireDate = new Date(now.getTime() + Long.parseLong(tokenExpireTime));

        return Jwts.builder()
                .subject(member.getMemberId().toString())   // 토큰 주체를 id로 설정
                .claim(AUTHORITIES_KEY, member.getRole().toString()) // 추가된 권한 Claim 
                .issuedAt(now)  // 발행 시간
                .expiration(expireDate) // 만료 시간
                .signWith(key, Jwts.SIG.HS256)  // ㅏ토큰 암호화
                .compact(); // 압축, 서명 후 토큰 생성
    }

    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parser()
                    .verifyWith(key)
                    .build()
                    .parseSignedClaims(token);  // 토큰 파싱, 검증
            return true;    // 검증 완료 -&gt; 유효한 토큰
            // 검증 실패 시 반환하는 예외에 따라 다르게 실행
        } catch (UnsupportedJwtException | MalformedJwtException e) {
            throw new BusinessException(ErrorCode.NO_AUTHORIZATION_EXCEPTION, &quot;JWT 가 유효하지 않습니다.&quot;);
        } catch (SignatureException e) {
            throw new BusinessException(ErrorCode.NO_AUTHORIZATION_EXCEPTION, &quot;JWT 서명 검증에 실패했습니다.&quot;);
        } catch (ExpiredJwtException e) {
            throw new BusinessException(ErrorCode.NO_AUTHORIZATION_EXCEPTION, &quot;JWT 가 만료되었습니다.&quot;);
        } catch (IllegalArgumentException e) {
            throw new BusinessException(ErrorCode.NO_AUTHORIZATION_EXCEPTION, &quot;JWT 가 null 이거나 비어있거나 공백만 있습니다.&quot;);
        } catch (Exception e) {
            throw new BusinessException(ErrorCode.NO_AUTHORIZATION_EXCEPTION, &quot;JWT 검증에 실패했습니다.&quot;);
        }
    }

    // 인증 객체 반환 core 로 임포트
    public Authentication getAuthentication(String token) {
        Claims claims = parseClaims(token);

        String authority = claims.get(AUTHORITIES_KEY, String.class);
        if(authority == null) {
            throw new BusinessException(ErrorCode.NO_AUTHORIZATION_EXCEPTION, &quot;권한 정보가 없는 토큰입니다.&quot;);
        }

        List&lt;GrantedAuthority&gt; authorities = Arrays.stream(authority.split(&quot;,&quot;))
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());

        // memberId, 공백, authorities 반환
        return new UsernamePasswordAuthenticationToken(claims.getSubject(), &quot;&quot;, authorities);
    }

    // Claims 파싱 
    private Claims parseClaims(String accessToken) {
        try{
            JwtParser parser = Jwts.parser()
                    .verifyWith(key)
                    .build();

            return parser.parseSignedClaims(accessToken).getPayload();
        }catch (ExpiredJwtException e){
            return e.getClaims();
        }
    }
}</code></pre>
<ul>
<li>getAuthentication : JWT 토큰에서 사용자 정보를 추출해 spring security가 사용하는 인증 객체를 생성합니다</li>
<li>parseClaims : JWT을 파싱하여 Claim(식별자, 권한 등)을 추출합니다</li>
</ul>
<h3 id="jwtauthorizationfilter">JwtAuthorizationFilter</h3>
<p>이 부분도 토큰 강의에서 했던 코드 그대로 갑니다!!</p>
<p>이 필터는 사용자의 요청마다 헤더에서 JWT를 추츨해 검증하고 유효한 경우 해당 사용자를 spring security 인증 객체로 등록합니다!</p>
<ul>
<li><p>JwtAuthorizationFilter</p>
<pre><code class="language-java">  package com.likelion.likelionjwt.global.jwt;

  import jakarta.servlet.FilterChain;
  import jakarta.servlet.ServletException;
  import jakarta.servlet.ServletRequest;
  import jakarta.servlet.ServletResponse;
  import jakarta.servlet.http.HttpServletRequest;
  import lombok.RequiredArgsConstructor;
  import org.springframework.security.core.Authentication;
  import org.springframework.security.core.context.SecurityContextHolder;
  import org.springframework.stereotype.Component;
  import org.springframework.util.StringUtils;
  import org.springframework.web.filter.GenericFilterBean;

  import java.io.IOException;

  @Component
  @RequiredArgsConstructor
  public class JwtAuthorizationFilter extends GenericFilterBean {

      private final JwtTokenProvider jwtTokenProvider;

      // 모든 요청이 들어올 때 필터가 가로채 인증 로직 실행
      @Override
      public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws
              IOException, ServletException {
          String token = resolveToken((HttpServletRequest) request); // 요청에서 토큰 resolve(추출)

          if (token != null &amp;&amp; jwtTokenProvider.validateToken(token)) {   // 토큰이 유효한지 확인
              Authentication authentication = jwtTokenProvider.getAuthentication(token);  // 유효하다면 토큰으로부터 인증 정보 가져옴
              SecurityContextHolder.getContext().setAuthentication(authentication);
              // SecurityContext에 인증 정보 저장 -&gt; 이후 요청 처리에서 인증된 사용자 정보에 접근할 수 있음
          }
          chain.doFilter(request, response); // 필터 체인의 다음 필터로 요청 넘기기
      }

      // 요청에서 토큰 추출 메소드
      private String resolveToken(HttpServletRequest request) {
          String bearerToken = request.getHeader(&quot;Authorization&quot;);    // 헤더에서 Authorization 값 꺼내기
          if (StringUtils.hasText(bearerToken) &amp;&amp; bearerToken.startsWith(&quot;Bearer &quot;)) {
              return bearerToken.substring(7);    // &quot;Bearer &quot; 부분을 잘라내고 토큰만 리턴
          }
          return null;
      }
  }</code></pre>
</li>
</ul>
<h3 id="authlogincontroller-1">AuthLoginController</h3>
<p>아까 만들었던 googleLogin부분을 주석처리 후 작성해 주세요!!</p>
<p>구글에서 받은 인가 코드를 받아 액세스 토큰 → 사용자 정보 → JWT 토큰 생성의 과정을 수행하고 클라이언트에게 JWT 액세스 토큰을 응답으로 전달합니다</p>
<pre><code class="language-java">package com.likelion.likelionjwt.oauth.api;

import com.likelion.likelionjwt.oauth.api.dto.Token;
import com.likelion.likelionjwt.oauth.application.AuthLoginService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/login/oauth2&quot;)
public class AuthLoginController {
    private final AuthLoginService authLoginService;

//    // code : 인증 서버에서 받은 authorization code
//    // registrationId : 사용자가 로그인한 소셜 로그인의 id
//    @GetMapping(&quot;/code/{registrationID}&quot;)
//    public void googleLogin(@RequestParam String code, @PathVariable String registrationID){
//        authLoginService.socialLogin(code, registrationID);
//    }

    @GetMapping(&quot;/code/google&quot;)
    public Token googleCallback(@RequestParam(name = &quot;code&quot;) String code){
        String googleAccessToken = authLoginService.getGoogleAccessToken(code);
        return loginOrSignup(googleAccessToken);
    }

    public Token loginOrSignup(String googleAccessToken){
        return authLoginService.loginOrSignUp(googleAccessToken);
    }
}</code></pre>
<ul>
<li>googleCallback은<ul>
<li>인가 코드를 통해 access token을 요청하고,</li>
<li>사용자 정보 조회 → JWT 발급 또는 회원가입 처리를 하고, (loginOrSignup)</li>
<li>Token객체를 JSON 형태로 응답합니다!</li>
</ul>
</li>
</ul>
<p><code>Cannot resolve method &#39;getGoogleAccessToken&#39; in &#39;AuthLoginService’</code></p>
<p><code>Cannot resolve method &#39;loginOrSignUp&#39; in &#39;AuthLoginService’</code></p>
<p>와 같은 오류가 뜨는 것은 정상이랍니다~</p>
<h3 id="authloginservice-1">AuthLoginService</h3>
<p>이것도 기존에 작성한 코드를 주석처리하고 작성해 주세요~</p>
<p>이 클래스는 OAuth 로그인 후 인가 코드를 통해 access token을 발급받고 사용자 정보를 조회해 회원가입 또는 로그인 처리 후 JWT를 생성해 반환하는 비즈니스 로직을 담당합니다!</p>
<pre><code class="language-java">package com.likelion.likelionjwt.oauth.application;

import com.google.gson.Gson;
import com.likelion.likelionjwt.global.jwt.JwtTokenProvider;
import com.likelion.likelionjwt.member.domain.Member;
import com.likelion.likelionjwt.member.domain.Role;
import com.likelion.likelionjwt.member.domain.repository.MemberRepository;
import com.likelion.likelionjwt.oauth.api.dto.Token;
import com.likelion.likelionjwt.oauth.api.dto.UserInfo;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.net.URI;
import java.util.Map;

@Service
@RequiredArgsConstructor
public class AuthLoginService {

//    // 컨트롤러에서 받은 authorization code와 소셜 로그인 id를 받아 콘솔에 출력
//    public void socialLogin(String code, String registrationId){
//        System.out.println(&quot;code = &quot; + code);
//        System.out.println(&quot;registrationId = &quot; + registrationId);
//    }

    @Value(&quot;${client-id}&quot;) // import 시 lombok으로 하면 안됨
    private String GOOGLE_CLIENT_ID;

    @Value(&quot;${client-secret}&quot;)
    private String GOOGLE_CLIENT_SECRET;

    // 구글 인증 코드를 엑세스 토큰으로 교환하는 API 주소
    private final String GOOGLE_TOKEN_URL = &quot;https://oauth2.googleapis.com/token&quot;;
    // OAuth 인증 후 구글이 리디렉션할 URI
    private final String GOOGLE_REDIRECT_URI = &quot;http://localhost:8080/login/oauth2/code/google&quot;;

    private final MemberRepository memberRepository;
    private final JwtTokenProvider jwtTokenProvider;

    public String getGoogleAccessToken(String code) {
        RestTemplate restTemplate = new RestTemplate();
        Map&lt;String, String&gt; params = Map.of(
                &quot;code&quot;, code,
                &quot;scope&quot;, &quot;https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email&quot;,
                &quot;client_id&quot;, GOOGLE_CLIENT_ID,
                &quot;client_secret&quot;, GOOGLE_CLIENT_SECRET,
                &quot;redirect_uri&quot;, GOOGLE_REDIRECT_URI,
                &quot;grant_type&quot;, &quot;authorization_code&quot;
        );

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

        if(responseEntity.getStatusCode().is2xxSuccessful()) {
            String json = responseEntity.getBody();
            Gson gson = new Gson();

            return gson.fromJson(json, Token.class)
                    .getAccessToken();
        }
        throw new RuntimeException(&quot;구글 엑세스 토큰을 가져오는데 실패했습니다.&quot;);
    }

    public Token loginOrSignUp(String googleAccessToken){
        UserInfo userInfo = getUserInfo(googleAccessToken);

        if(!userInfo.getVerifiedEmail()){
            throw new RuntimeException(&quot;이메일 인증이 되지 않은 유저입니다.&quot;);
        }

        Member member = memberRepository.findByEmail(userInfo.getEmail()).orElseGet(() -&gt;
                memberRepository.save(Member.builder()
                        .email(userInfo.getEmail())
                        .name(userInfo.getName())
                        .pictureUrl(userInfo.getPictureUrl())
                        .role(Role.ROLE_USER)
                        .build())
        );

        String jwt = jwtTokenProvider.generateToken(member);
        return new Token(jwt);
    }

    public UserInfo getUserInfo(String accessToken){
        RestTemplate restTemplate = new RestTemplate();
        String url = &quot;https://www.googleapis.com/oauth2/v2/userinfo?access_token=&quot; + accessToken;

        HttpHeaders headers = new HttpHeaders();
        headers.set(&quot;Authorization&quot;, &quot;Bearer &quot; + accessToken);
        headers.setContentType(MediaType.APPLICATION_JSON);

        RequestEntity&lt;Void&gt; requestEntity = new RequestEntity&lt;&gt;(headers, HttpMethod.GET, URI.create(url));
        ResponseEntity&lt;String&gt; responseEntity = restTemplate.exchange(requestEntity, String.class);

        if(responseEntity.getStatusCode().is2xxSuccessful()) {
            String json = responseEntity.getBody();
            Gson gson = new Gson();
            return gson.fromJson(json, UserInfo.class);
        }

        throw new RuntimeException(&quot;유저 정보를 가져오는데 실패했습니다.&quot;);
    }
}</code></pre>
<ul>
<li>getGoogleAccessToken에서는<ul>
<li>Google OAuth 토큰 서버에 Post 요청을 보내 access_token을 받아옵니다!</li>
<li>RestTemplate를 사용해 파라미터를 전송하고</li>
<li>응답 성공 시 JSON을 파싱해 Token객체를 만들고 access_token을 반환합니다!</li>
</ul>
</li>
<li>getUserInfo에서는<ul>
<li>구글 사용자 정보 API를 호출합니다! (<a href="https://www.googleapis.com/oauth2/v2/userinfo">https://www.googleapis.com/oauth2/v2/userinfo</a>)</li>
<li>헤더에 access token을 받아 GET 요청을 하고</li>
<li>응답으로 받은 JSON을 UserInfo 객체로 파싱합니다!</li>
</ul>
</li>
<li>loginOrSignUp에서는<ul>
<li>getUserInfo로 사용자 정보를 가져온 뒤 이메일 인증 여부를 확인하고</li>
<li>DB에 사용자가 없으면 회원가입 후 저장, 있으면 그대로 로그인 합니다!</li>
<li>JwtTokenProvider를 통해 JWT토큰을 생성해여 Token으로 반환합니다!</li>
</ul>
</li>
</ul>
<p>이제 발급받은 Authorization code를 통해 Access Token을 발급받아보겠습니당!</p>
<aside>
💡

<p><a href="https://accounts.google.com/o/oauth2/v2/auth?client_id=">https://accounts.google.com/o/oauth2/v2/auth?client_id=</a><CLIENT_ID>&amp;redirect_uri=<REDIRECT_URI>&amp;response_type=code&amp;scope=email%20profile</p>
<p>다시 아까 작성했던 링크로 들어가 로그인 해주세요!!</p>
</aside>

<p>그러면 이렇게 Access Token이 뜰겁니당!!</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/fa762373-4607-4304-94e0-7b4748fe4b7d/image.png" alt=""></p>
<p>이제 mysql로 들어가서 조회해볼건데용!</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/1bb2ca8e-e4b6-443e-9cb8-fe19f52ceec1/image.png" alt=""></p>
<p>위 사진처럼 회원가입이 되면서 이름, 이메일, 프로필 등 잘 뜨면 성공입니당~~</p>
<h3 id="오늘도-너무-수고-많으셨습니다">오늘도 너무 수고 많으셨습니다~</h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[CRUD 복습해보기]]></title>
            <link>https://velog.io/@daun_jung/CRUD-%EB%B3%B5%EC%8A%B5%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@daun_jung/CRUD-%EB%B3%B5%EC%8A%B5%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Thu, 10 Jul 2025 14:04:42 GMT</pubDate>
            <description><![CDATA[<p>멋쟁이 사자처럼 13기 강의를 진행하며 만든 자료를 블로그에 공유합니다.</p>
<h1 id="crud와-예외처리-이론-복습">CRUD와 예외처리 이론 복습</h1>
<h2 id="crud">CRUD</h2>
<table>
<thead>
<tr>
<th>Create</th>
<th>데이터 생성</th>
<th>POST</th>
</tr>
</thead>
<tbody><tr>
<td>Read</td>
<td>데이터 조회</td>
<td>GET</td>
</tr>
<tr>
<td>Update</td>
<td>데이터 수정</td>
<td>PUT / PATCH</td>
</tr>
<tr>
<td>Delete</td>
<td>데이터 삭제</td>
<td>DELETE</td>
</tr>
</tbody></table>
<h3 id="domainentity-클래스">Domain(Entity 클래스)</h3>
<p>도메인은 실제 데이터베이스와 매핑되며 시스템의 핵심 데이터를 표현하는 클래스</p>
<p>각 필드는 데이터베이스의 컬럼과 연결되며, 비즈니스 중심의 핵심 데이터를 담고 있다</p>
<ul>
<li><p><a href="http://Member.java">Member</a>(예은님 CRUD 강의자료 참고)</p>
<pre><code class="language-java">  package com.likelion.likelioncrud.member.domain;

  import com.likelion.likelioncrud.member.api.dto.request.MemberUpdateRequestDto;
  import com.likelion.likelioncrud.post.domain.Post;
  import jakarta.persistence.*;
  import lombok.AccessLevel;
  import lombok.Builder;
  import lombok.Getter;
  import lombok.NoArgsConstructor;

  import java.util.ArrayList;
  import java.util.List;

  @Entity
  @Getter
  @NoArgsConstructor(access = AccessLevel.PROTECTED)
  public class Member {

      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Column(name = &quot;member_id&quot;)
      private Long memberId;

      private String name;

      private int age;

      @Enumerated(EnumType.STRING)
      @Column(length = 20)
      private Part part;

      @OneToMany(mappedBy = &quot;member&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
      private List&lt;Post&gt; posts = new ArrayList&lt;&gt;();

      @Builder
      private Member(String name, int age, Part part) {
          this.name = name;
          this.age = age;
          this.part = part;
      }

      public void update(MemberUpdateRequestDto memberUpdateRequestDto) {
          this.name = memberUpdateRequestDto.name();
          this.age = memberUpdateRequestDto.age();
      }
  }</code></pre>
</li>
</ul>
<h3 id="repository">Repository</h3>
<p>데이터베이스에 직접 접근하는 계층으로, Spring Data JPA가 제공하는 <code>JpaRepository</code>를 상속받아 기본적인 CRUD 메서드를 사용할 수 있다</p>
<p>도메인 객체를 저장하거나 조회할 때 이 계층을 사용한다</p>
<ul>
<li><p>MemberRepository (예은님 CRUD 강의자료 참고)</p>
<pre><code class="language-java">  package com.likelion.likelioncrud.member.domain.repository;

  import com.likelion.likelioncrud.member.domain.Member;
  import org.springframework.data.jpa.repository.JpaRepository;

  public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
  }</code></pre>
</li>
</ul>
<h3 id="dto">DTO</h3>
<p>DTO는 클라이언트와 서버 간에 데이터를 전송하기 위한 객체이다</p>
<p>요청(Request)과 응답(Response)에 따라 서로 다른 구조로 설계한다</p>
<ul>
<li><p>MemberSaveRequestDto (예은님 CRUD 강의자료 참고)</p>
<pre><code class="language-java">  package com.likelion.likelioncrud.member.api.dto.request;

  import com.likelion.likelioncrud.member.domain.Part;

  public record MemberSaveRequestDto(
          String name,
          int age,
          Part part
  ) {
  }</code></pre>
</li>
</ul>
<h3 id="service">Service</h3>
<p>비즈니스 로직(실제 처리 내용)을 수행하는 계층이다</p>
<p>데이터 저장, 조회, 연산 등 핵심 처리 담당</p>
<ul>
<li><p>MemberService (예은님 CRUD 강의자료 참고)</p>
<pre><code class="language-java">  package com.likelion.likelioncrud.member.application;

  import java.util.List;

  import com.likelion.likelioncrud.member.api.dto.request.MemberSaveRequestDto;
  import com.likelion.likelioncrud.member.api.dto.request.MemberUpdateRequestDto;
  import com.likelion.likelioncrud.member.api.dto.response.MemberInfoResponseDto;
  import com.likelion.likelioncrud.member.api.dto.response.MemberListResponseDto;
  import com.likelion.likelioncrud.member.domain.Member;
  import com.likelion.likelioncrud.member.domain.repository.MemberRepository;
  import lombok.RequiredArgsConstructor;
  import org.springframework.stereotype.Service;
  import org.springframework.transaction.annotation.Transactional;

  @Service
  @RequiredArgsConstructor
  @Transactional(readOnly = true)
  public class MemberService {
      private final MemberRepository memberRepository;

      // 사용자 정보 저장
      @Transactional
      public void memberSave(MemberSaveRequestDto memberSaveRequestDto) {
          Member member = Member.builder()
                  .name(memberSaveRequestDto.name())
                  .age(memberSaveRequestDto.age())
                  .part(memberSaveRequestDto.part())
                  .build();
          memberRepository.save(member);
      }

      // 사용자 모두 조회
      public MemberListResponseDto memberFindAll() {
          List&lt;Member&gt; members = memberRepository.findAll();
          List&lt;MemberInfoResponseDto&gt; memberInfoResponseDtoList = members.stream()
                  .map(MemberInfoResponseDto::from)
                  .toList();
          return MemberListResponseDto.from(memberInfoResponseDtoList);
      }

      // 단일 사용자 조회
      public MemberInfoResponseDto memberFindOne(Long memberId) {
          Member member = memberRepository
                  .findById(memberId)
                  .orElseThrow(IllegalArgumentException::new);
          return MemberInfoResponseDto.from(member);
      }

      // 사용자 정보 수정
      @Transactional
      public void memberUpdate(Long memberId, MemberUpdateRequestDto memberUpdateRequestDto) {
          Member member = memberRepository.findById(memberId).orElseThrow(IllegalArgumentException::new);
          member.update(memberUpdateRequestDto);
      }

      // 사용자 정보 삭제
      @Transactional
      public void memberDelete(Long memberId) {
          Member member = memberRepository.findById(memberId).orElseThrow(IllegalArgumentException::new);
          memberRepository.delete(member);
      }
  }</code></pre>
</li>
</ul>
<h3 id="controller">Controller</h3>
<p>사용자의 HTTP 요청을 받아 처리하고 응답을 돌려주는 역할</p>
<p>적절한 HTTP 메서드와 함께 <code>@RequestMapping</code>, <code>@PostMapping</code>, <code>@GetMapping</code> 등을 사용하여 API를 정의한다</p>
<ul>
<li><p>MemberController (예은님 CRUD 강의자료 참고)</p>
<pre><code class="language-java">  package com.likelion.likelioncrud.member.api;

  import com.likelion.likelioncrud.member.api.dto.request.MemberSaveRequestDto;
  import com.likelion.likelioncrud.member.api.dto.request.MemberUpdateRequestDto;
  import com.likelion.likelioncrud.member.api.dto.response.MemberInfoResponseDto;
  import com.likelion.likelioncrud.member.api.dto.response.MemberListResponseDto;
  import com.likelion.likelioncrud.member.application.MemberService;
  import lombok.RequiredArgsConstructor;
  import org.springframework.http.HttpStatus;
  import org.springframework.http.ResponseEntity;
  import org.springframework.web.bind.annotation.*;

  @RestController
  @RequiredArgsConstructor
  @RequestMapping(&quot;/member&quot;)
  public class MemberController {

      private final MemberService memberService;

      // 사용자 저장
      @PostMapping(&quot;/save&quot;)
      public ResponseEntity&lt;String&gt; memberSave(@RequestBody MemberSaveRequestDto memberSaveRequestDto) {
          memberService.memberSave(memberSaveRequestDto);
          return new ResponseEntity&lt;&gt;(&quot;사용자 저장!&quot;, HttpStatus.CREATED);
      }

      // 사용자 전체 조회
      @GetMapping(&quot;/all&quot;)
      public ResponseEntity&lt;MemberListResponseDto&gt; memberFindAll() {
          MemberListResponseDto memberListResponseDto = memberService.memberFindAll();
          return new ResponseEntity&lt;&gt;(memberListResponseDto, HttpStatus.OK);
      }

      // 회원 id를 통해 특정 사용자 조회
      @GetMapping(&quot;/{memberId}&quot;)
      public ResponseEntity&lt;MemberInfoResponseDto&gt; memberFindOne(@PathVariable(&quot;memberId&quot;) Long memberId) {
          MemberInfoResponseDto memberInfoResponseDto = memberService.memberFindOne(memberId);
          return new ResponseEntity&lt;&gt;(memberInfoResponseDto, HttpStatus.OK);
      }

      // 회원 id를 통한 사용자 수정
      @PatchMapping(&quot;/{memberId}&quot;)
      public ResponseEntity&lt;String&gt; memberUpdate(@PathVariable(&quot;memberId&quot;) Long memberId,
                                                 @RequestBody MemberUpdateRequestDto memberUpdateRequestDto) {
          memberService.memberUpdate(memberId, memberUpdateRequestDto);
          return new ResponseEntity&lt;&gt;(&quot;사용자 수정&quot;, HttpStatus.OK);
      }

      // 회원 id를 통한 사용자 삭제
      @DeleteMapping(&quot;/{memberId}&quot;)
      public ResponseEntity&lt;String&gt; memberDelete(@PathVariable(&quot;memberId&quot;) Long memberId) {
          memberService.memberDelete(memberId);
          return new ResponseEntity&lt;&gt;(&quot;사용자 삭제&quot;, HttpStatus.OK);
      }
  }</code></pre>
</li>
</ul>
<h2 id="rest-api-설계-원칙">Rest API 설계 원칙</h2>
<p>URI는 정보의 리소스를 표현해야 한다</p>
<pre><code class="language-java">// REST를 제대로 적용하지 않은 URI 예시
GET /members/delete/1</code></pre>
<ul>
<li>행위에 대한 표현이 아닌 리소스를 표현하는데 중점을 두어야 한다</li>
<li>리소스명은 동사보다는 명사를 사용한다</li>
</ul>
<pre><code class="language-java">// REST 적용 예시 1
GET /members/delete/1 (X)
DELETE /members/1 (O)

// REST 적용 예시 2
GET /members/show/1 (X)
GET /members/1 (O)

// REST 적용 예시 3
GET /members/insert/3 (X)
POST /members/3 (O)</code></pre>
<ul>
<li>슬래시 구분자(/)는 계층 관계를 나타내는데 사용한다</li>
<li>URI 마지막 문자로 슬래시(/)를 표한하지 않는다</li>
<li>하이픈(-)은 URI 가독성을 높이는데 사용할 수 있다</li>
<li>언더바(_)는 URI에 사용하지 않는다</li>
<li>URI 경로에는 소문자를 사용한다</li>
</ul>
<p>EX) REST는 자원 중심 설계 지향 → /getUser는 REST 위반</p>
<p> 올바른 방식 : /user/{id} + GET</p>
<h2 id="예외-상황별-http-상태코드">예외 상황별 HTTP 상태코드</h2>
<table>
<thead>
<tr>
<th>예외 상황</th>
<th>HTTP 코드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>잘못된 입력값</td>
<td>400 Bad Request</td>
<td>필드 유효성 검증 실패</td>
</tr>
<tr>
<td>리소스 없음</td>
<td>404 Not Found</td>
<td>존재하지 않는 ID</td>
</tr>
<tr>
<td>서버 내부 오류</td>
<td>500 Internal Server Error</td>
<td>NullPointer, DB 다운 등</td>
</tr>
<tr>
<td>인증 실패</td>
<td>401 Unauthorized</td>
<td>JWT 누락, 로그인 필요</td>
</tr>
<tr>
<td>권한 없음</td>
<td>403 Forbidden</td>
<td>접근 권한 없음</td>
</tr>
</tbody></table>
<h2 id="예외-처리">예외 처리</h2>
<h3 id="errorcode-successcode-정의">ErrorCode, SuccessCode 정의</h3>
<p>프로젝트 전반에서 사용될 성공, 실패 응답 코드를 한 곳에 정의</p>
<p>응답 메시지, 상태 코드, 코드명을 함께 정의하여 일관된 응답을 생성</p>
<p>예시:</p>
<ul>
<li><code>NOT_FOUND_404(&quot;해당 리소스를 찾을 수 없습니다.&quot;)</code></li>
<li><code>INTERNAL_SERVER_ERROR_500(&quot;서버 내부 오류입니다.&quot;)</code></li>
</ul>
<h3 id="businessexception--커스텀-예외-클래스">BusinessException : 커스텀 예외 클래스</h3>
<p>비즈니스 로직 중 발생 가능한 예외를 처리하기 위한 커스텀 예외 클래스</p>
<p>각 예외에 적절한 ErrorCode를 전달하여 일관된 응답 제공</p>
<ul>
<li><p>BusinessException(하영님 예외처리 강의자료 참고)</p>
<pre><code class="language-java">  package com.likelion.likelioncrud.common.exception;

  import com.likelion.likelioncrud.common.error.ErrorCode;
  import lombok.Getter;

  @Getter // getter 메소드 자동 생성 lombok 어노테이션
  public class BusinessException extends RuntimeException {

      private final ErrorCode errorCode;   // 에러 코드 ex) NOT_FOUND
      private final String customMessage;  // 사용자 정의 예외 메시지(커스텀)

          // 생성자
      public BusinessException(ErrorCode errorCode, String customMessage) {
          super(customMessage);    // RuntimeException의 생성자에 메시지 전달
          this.errorCode = errorCode;
          this.customMessage = customMessage;
      }
  }</code></pre>
</li>
</ul>
<h3 id="apirestemplate--공통-응답-포맷">ApiResTemplate : 공통 응답 포맷</h3>
<p>모든 API 응답을 동일한 포맷으로 구성하기 위한 템플릿</p>
<p><code>code</code>, <code>message</code>, <code>data</code> 구조</p>
<ul>
<li><p>ApiResTemplate(하영님 예외처리 강의자료 참고)</p>
<pre><code class="language-java">  package com.likelion.likelioncrud.common.template;

  import com.likelion.likelioncrud.common.error.ErrorCode;
  import com.likelion.likelioncrud.common.error.SuccessCode;
  import lombok.*;

  @Getter // getter 메소드 자동 생성 lombok 어노테이션
  @AllArgsConstructor(access = AccessLevel.PRIVATE) // 모든 필드 값을 매개변수로 받는 생성자 자동 생성
  @RequiredArgsConstructor(access = AccessLevel.PRIVATE) // final 붙은 필드만 매개변수로 받는 생성자 자동 생성
  @Builder // 빌더 패턴
  public class ApiResTemplate&lt;T&gt; {

      private final int code;       // 응답 코드 (200, 404 등)
      private final String message; // 응답 메시지
      private T data;               // 응답 데이터 (제네릭 형식으로 다양한 타입 수용 가능)

      // 데이터 없는 성공 응답
      public static ApiResTemplate successWithNoContent(SuccessCode successCode) {
          return new ApiResTemplate&lt;&gt;(successCode.getHttpStatusCode(), successCode.getMessage());
      }

      // 데이터 포함한 성공 응답
      public static &lt;T&gt; ApiResTemplate&lt;T&gt; successResponse(SuccessCode successCode, T data) {
          return new ApiResTemplate&lt;&gt;(successCode.getHttpStatusCode(), successCode.getMessage(), data);
      }

      // 에러 응답 (커스텀 메시지 포함)
      public static ApiResTemplate errorResponse(ErrorCode errorCode, String customMessage) {
          return new ApiResTemplate&lt;&gt;(errorCode.getHttpStatusCode(), customMessage);
      }

  }
</code></pre>
</li>
</ul>
<h3 id="customexceptionadvice--전역-예외-처리기">CustomExceptionAdvice : 전역 예외 처리기</h3>
<p>커스텀 예외 클래스를 정의하고 이를 처리</p>
<p>모든 컨트롤러에서 발생하는 예외를 한 곳에서 처리</p>
<ul>
<li><p>CustomExceptionAdvice(하영님 예외처리 강의자료 참고)</p>
<pre><code class="language-java">  package com.likelion.likelioncrud.common.exception;

  import com.likelion.likelioncrud.common.error.ErrorCode;
  import com.likelion.likelioncrud.common.exception.BusinessException;
  import com.likelion.likelioncrud.common.template.ApiResTemplate;
  import lombok.RequiredArgsConstructor;
  import lombok.extern.slf4j.Slf4j;
  import org.springframework.http.HttpStatus;
  import org.springframework.http.ResponseEntity;
  import org.springframework.stereotype.Component;
  import org.springframework.web.bind.annotation.*;

  @Slf4j // 로깅을 위한 Logger를 생성
  @RestControllerAdvice // REST API 컨트롤러에 대한 예외 처리 어드바이스임을 나타내는 어노테이션
  @Component // 클래스를 Spring 컴포넌트로 등록
  @RequiredArgsConstructor
  public class CustomExceptionAdvice {

      /**
       * 500 Internal Server Error
       * 원인 모를 이유의 예외 발생 시
       * 모든 종류의 Exception 처리
       */
      @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // HTTP 500 상태 코드 반환 어노테이션
      @ExceptionHandler(Exception.class)  // 모든 종류의 Exception 처리
      public ApiResTemplate handleServerException(final Exception e) {
          log.error(&quot;Internal Server Error: {}&quot;, e.getMessage(), e); // 로그 출력
          return ApiResTemplate.errorResponse(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage());
      }

      /**
       * custom error
       * 내부 커스텀 에러
       * BusinessException 처리
       */
      @ExceptionHandler(BusinessException.class)
      public ResponseEntity&lt;ApiResTemplate&gt; handleCustomException(BusinessException e) {
          log.error(&quot;CustomException: {}&quot;, e.getMessage(), e); // 로그 출력

          ApiResTemplate&lt;?&gt; body = ApiResTemplate.errorResponse(e.getErrorCode(), e.getMessage());

          // 에러 코드에 정의된 HTTP 상태 코드로 응답
          return ResponseEntity
                  .status(e.getErrorCode().getHttpStatus())
                  .body(body);
      }

  }
</code></pre>
</li>
</ul>
<h1 id="자기참조-관계순환관계">자기참조 관계(순환관계)</h1>
<p>엔티티의 하나의 데이터가 같은 엔티티의 다른 데이터와 관계를 가지는 것</p>
<p>즉, 엔티티가 자기 자신과 관계를 가지는 것</p>
<p>EX)</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/939b5f6f-48b6-4331-8293-b2cfc619f552/image.png" alt=""></p>
<p>학생의 학부모가 한 명 있다고 가정하면 위 사진처럼 관계를 맺는다</p>
<p>하지만 학생이 자라서 학부모가 된다면 위 모델은 수용할 수 없다</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/2a773d23-f3fb-456e-93ec-ab341bc266d0/image.png" alt=""></p>
<p>그러면 위 사진처럼 학생 밑에 새로운 엔티티를 추가하면 해결이 가능하다</p>
<p>하지만 학생_1이 다시 자라 학부모가 된다면 이 모델은 다시 수용할 수 없어진다</p>
<p>수용하려면 다시 엔티티를 추가해야하는데 좋은 모델이란 엔티티의 최소한의 변경으로 변화에 대응해야 한다</p>
<p>이럴 때 사용하는 것이 자기참조관계이다</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/5f5ea764-abb4-4666-9e9b-64eafaa95faa/image.png" alt=""></p>
<p>학부모와 학생은 모두 사람이기에 통합하고 자기참조관계선을 추가해 학생의 학부모가 누구인지 관리할 수 있다</p>
<p>여기서 필수적인 관계로 만들면 최상위 부모 개체가 만들어 질 수 없기에 Null값을 허용한 관계선을 설정해야 한다</p>
<h1 id="다대다nm-관계">다대다(N:M) 관계</h1>
<h3 id="manytomany에-대하여">@ManyToMany에 대하여</h3>
<p>JPA에서는 두 엔티티 간의 다대다 관계를 맺을 때 @ManyToMany 어노테이션을 사용할 수 있다</p>
<p>하지만 실무에서는 이를 직접 사용하는 것을 지양한다 → </p>
<ul>
<li>실무에서는 다대다 관계를 단순 연결로 끝내지 않고 추가 정보를 연결 테이블에 넣는 경우가 많다</li>
<li>예를 들어 회원과 상품 간의 다대다 관계를 생각해보면 우리는 주문 이라는 중간 테이블을 만들어 사용한다</li>
<li>이 주문 테이블에는 단순히 회원 ID, 상품 ID만 있는 것이 아니라, 주문 수량, 주문 일시, 상태 같은 추가 정보도 함께 저장되어야 한다</li>
<li>그런데 @ManyToMany를 그대로 사용하면 JPA는 중간 테이블을 엔티티로 만들지 않고 단순 연결만하기에 이런 추가 정보를 다룰 수 없다</li>
</ul>
<p>⇒ 따라서 다대다 관계를 일대다 - 다대일 관계로 풀어서 구현한다</p>
<ul>
<li>중간 테이블을 명시적인 엔티티로 만든다</li>
<li>그 엔티티 내부에서 @ManyToOne, @OneToMany 를 사용해 각 엔티티를 연결한다</li>
</ul>
<p>따라서 @ManyToMany 를 직접 사용하지 않고 중간 엔티티(PostTag)를 만들어 </p>
<ul>
<li><code>게시글(Post)</code> ↔ <code>게시글-태그(PostTag)</code> (일대다)</li>
<li><code>태그(Tag)</code> ↔ <code>게시글-태그(PostTag)</code> (일대다)</li>
</ul>
<p>로 나누어 구현해보겠습니다~</p>
<h1 id="라이브-코딩-실습">라이브 코딩 실습</h1>
<p>멋쟁이 사자처럼 강의 자료와 운영진에게 질문을 통해 실습을 해 보아요~</p>
<p>기존 crud와 예외처리, 폼 검증을 하던 프로젝트에 이어서 진행할게요!</p>
<h3 id="오늘의-테스트-목표">오늘의 테스트 목표</h3>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/152e5547-9767-4594-b69d-af39b101245d/image.png" alt=""></p>
<p>post 조회 시 tag들을 함께 출력</p>
<p>PostTag라는 중간 엔티티를 이용해 다대다 관계를 설정</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/c8ed6980-96b2-463c-a3a7-ed50210f6e02/image.png" alt=""></p>
<p>crud 강의와 동일하게 tag의 저장, 전체 조회, 단건 조회, 수정, 삭제</p>
<p>→ 오늘 목표를 강의 시간에 다 만드려니 시간이 너무 짧죠??</p>
<p>그래서 오늘 강의 시간 목표는 따로 있고, 나머지 목표들은 과제로 해 주시면 됩니다!</p>
<p>오늘의 목표를 완성하신 분들은 귀가하시면 됩니다~ 그러니 운영진들에게 질문하며 진행해 주세요! 지피티는 사용하지 않기!!</p>
<hr>
<h3 id="오늘의-강의시간-목표">오늘의 강의시간 목표</h3>
<p>게시글(Post)에는 여러 개의 태그(Tag)를 달 수 있습니다!</p>
<p>예를 들어 “Spring”, “CRUD”같은 태그를 붙일 수 있겠죠?</p>
<p>이런 구조는 다대다 관계이지만 우리는 Post와 Tag사이에 PostTag라는 중간 엔티티를 사용하여 설계하겠습니다!!</p>
<ul>
<li><p>Tag엔티티와 PostTag 엔티티를 작성해 주세요</p>
</li>
<li><p>Post엔티티는 이미 CRUD 강의 시간에 작성했으니 그 코드에 Post와의 연관관계를 추가하시면 됩니다</p>
</li>
<li><p>&lt;힌트&gt;</p>
<p>  Ex)</p>
<pre><code class="language-java">  @NoArgsConstructor(access = AccessLevel.PROTECTED)</code></pre>
<ul>
<li><p>아무런 매개변수가 없는 생성자를 생성하되 같은 패키지나 하위 클래스에서만 접근 가능</p>
</li>
<li><p>JPA가 클래스의 기본 생성자를 호출해서 객체를 만들고 그 위에 프록시를 씌워 지연 로딩을 가능하게 함</p>
</li>
<li><p>AllArgsConstructor, RequiredArgsConstructor</p>
<pre><code class="language-java">  @AllArgsConstructor // 모든 필드 값을 파라미터로 받는 생성자를 생성
  @RequiredArgsConstructor // final이나 @NonNull으로 선언된 필드만을 파라미터로 받는 생성자를 생성</code></pre>
</li>
</ul>
</li>
</ul>
<pre><code>```java
@Id // 기본 키(PK)를 지정
```

```java
@GeneratedValue(strategy = GenerationType.IDENTITY)
```

- GeneratedValue 어노테이션은 기본 키 값을 자동 생성
- straregy의 속성은 4가지
    - AUTH : 특정 DB에 맞게 자동 선택(기본값)
    - IDENTITY : 기본 키 생성을 데이터베이스에 위임(ex.MYSQL등)
    - SEQUENCE : 데이터베이스 시퀀스를 사용해 기본키 할당(ex.ORACLE, H2등)
    - TABLE : 키 생성 전용 테이블을 만들어 SEQUENCE처럼 사용(ex. 모든 DBMS)

```java
@Column(name = &quot;member_id&quot;, nullable = false, length = 10)
```

- Column어노테이션의 속성
    - name : DB 컬럼명 지정
    - nullable : null 허용 여부
    - length : 문자 길이 제한

```java
@ManyToOne(fetch = FetchType.LAZY)
```

- fetch = FetchType.Lazy → 지연 로딩 : 연관 관계를 즉시 로딩하지 않고 실제로 사용할 때 DB에서 조회 (성능 최적화)
- 연관된 엔티티를 프록시 객체로 생성해 두었다가 실제 접근이 일어날 때 DB에서 로딩
- 프록시 : 실제 엔티티 객체 대신 데이터베이스 조회를 지연할 수 있는 가짜 객체
- 즉시 로딩

    ```java
    @ManyToOne(fetch = FetchType.EAGER)
    ```

- 즉시 로딩을 사용하게 되면 예를 들어 연관된 엔티티의 데이터를 수만 건 등록했을 때, 연간 된 엔티티를 로딩하는 수간 수만 건의 데이터가 함께 로딩될 것 → 그렇기에 연관된 엔티티가 하나면 즉시 로딩을, 아니라면 지연 로딩을 사용
- ManyToOne, OneToMany에서는 지연 로딩이 권장됨

```java
@JoinColumn(name = &quot;member_id&quot;)
```

- JoinColumn은 엔티티의 연관관계에서 외래 키를 매핑하기 위해 사용됨
- 내가 외래 키를 가지고 있을 때, 그 외래 키가 어떤 컬럼으로 매핑될지 지정함
- name속성은 매핑할 외래 키의 이름을 지정함
- +속성
    - nullable : 외래 키의 NULL 허용 여부
    - unique : 유일성 제약 여부

- 결론적으로 참조하는 엔티티가 존재할 경우 JoinColumn으로 FK를 매핑하는 필드의 이름과 대상 엔티티의 PK로 FK 이름이 지정됨
- JoinColumn은 외래 키를 소유하는 쪽에서 사용, 반대편에서는 mappedBy 사용
- 외래 키를 직접 소유 (vs mappedBy)

```java
@OneToMany(mappedBy = &quot;post&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
```

- mappedBy = &quot;post&quot; → PostTag 엔티티의 post 필드가 연관관계의 주인
- cascade = ALL → 게시글 저장/삭제 시 PostTag도 함께 처리됨
- orphanRemoval = true → Post에서 PostTag 제거 시 DB에서도 삭제됨</code></pre><p><img src="https://velog.velcdn.com/images/daun_jung/post/d9c2a5bc-2c28-4425-b541-82000bfc79ab/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/3a5c7c11-5040-40d2-822c-c5336b277d2d/image.png" alt=""></p>
<p><strong>Post 엔티티</strong></p>
<ul>
<li><p>하나의 Member는 여러 Post와 연결되어야 한다(N:1)</p>
</li>
<li><p>하나의 Post는 여러 PostTag와 연결되어야 한다(1:N)</p>
</li>
<li><p>postId는 PK이며 자동 증가되어야 한다</p>
</li>
<li><p>title, contents는 반드시 존재해야 한다</p>
</li>
<li><p>Member는 반드시 있어야 하며 지연로딩으로 설정해야 한다</p>
</li>
<li><p>PostTag는 Post와 함께 저장, 삭제 된다</p>
</li>
<li><p>과제 시 참고</p>
<pre><code class="language-java">  package com.likelion.likelioncrud.post.domain;

  import com.likelion.likelioncrud.member.domain.Member;
  import com.likelion.likelioncrud.post.api.dto.request.PostUpdateRequestDto;
  import com.likelion.likelioncrud.posttag.domain.PostTag;
  import jakarta.persistence.*;
  import lombok.AccessLevel;
  import lombok.Builder;
  import lombok.Getter;
  import lombok.NoArgsConstructor;

  import java.util.ArrayList;
  import java.util.List;

  @Entity
  @Getter
  @NoArgsConstructor(access = AccessLevel.PROTECTED)
  public class Post {

      @Id
      @Column(name = &quot;post_id&quot;)
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long postId;

      @Column(nullable = false)
      private String title;

      @Column(nullable = false)
      private String contents;

      @ManyToOne(fetch = FetchType.LAZY)
      @JoinColumn(name = &quot;member_id&quot;, nullable = false)
      private Member member;

      // 일대다 관계 하나의 게시글은 여러 PostTag와 연결 가능
      @OneToMany(mappedBy = &quot;post&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
      private List&lt;PostTag&gt; postTags = new ArrayList&lt;&gt;();

      @Builder
      private Post(String title, String contents, Member member) {
          this.title = title;
          this.contents = contents;
          this.member = member;
      }

      public void update(PostUpdateRequestDto postUpdateRequestDto) {
          this.title = postUpdateRequestDto.title();
          this.contents = postUpdateRequestDto.contents();
      }
  }</code></pre>
</li>
</ul>
<p><strong>Tag 엔티티</strong></p>
<ul>
<li><p>하나의 Tag는 여러 PostTag와 연결되어야 한다(1:N)</p>
</li>
<li><p>tagId는 기본키이며, 자동 생성되어야 한다</p>
</li>
<li><p>name 속성을 가지며 반드시 존재해야 한다</p>
</li>
<li><p>PostTag는 Tag와 함께 저장, 삭제 된다</p>
</li>
<li><p>과제 시 참고</p>
<pre><code class="language-java">  package com.likelion.likelioncrud.tag.domain;

  import com.likelion.likelioncrud.posttag.domain.PostTag;
  import com.likelion.likelioncrud.tag.api.dto.request.TagUpdateRequestDto;
  import jakarta.persistence.*;
  import lombok.AccessLevel;
  import lombok.Builder;
  import lombok.Getter;
  import lombok.NoArgsConstructor;

  import java.util.ArrayList;
  import java.util.List;

  @Entity
  @Getter
  @NoArgsConstructor(access = AccessLevel.PROTECTED)
  public class Tag {

      @Id
      @Column(name = &quot;tag_id&quot;)
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;

      @Column(nullable = false)
      private String name;

      // PostTag와의 일대다 연관관계
      @OneToMany(mappedBy = &quot;tag&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
      private List&lt;PostTag&gt; postTags = new ArrayList&lt;&gt;();

      @Builder
      public Tag(String name) {
          this.name = name;
      }

      public void update(TagUpdateRequestDto tagUpdateRequestDto) {
          this.name = tagUpdateRequestDto.name();
      }
  }
</code></pre>
</li>
</ul>
<p><strong>PostTag 엔티티</strong></p>
<ul>
<li><p>여러 PostTag는 하나의  Post에 연결되어야 한다(N:1)</p>
</li>
<li><p>여러 PostTag는 하나의  Tag에 연결되어야 한다(N:1)</p>
</li>
<li><p>postTagId는 PK이며 자동 생성되어야 한다</p>
</li>
<li><p>post는 FK(외래키)이름을 지정해주고 반드시 존재해야하며 지연로딩을 설정해야 한다</p>
</li>
<li><p>tag는 FK(외래키)이름을 지정해주고 반드시 존재해야하며 지연로딩을 설정해야 한다</p>
</li>
<li><p>과제 시 참고</p>
<pre><code class="language-java">  package com.likelion.likelioncrud.posttag.domain;

  import com.likelion.likelioncrud.post.domain.Post;
  import jakarta.persistence.*;
  import lombok.AccessLevel;
  import lombok.Getter;
  import lombok.NoArgsConstructor;
  import com.likelion.likelioncrud.tag.domain.Tag;

  @Entity
  @Getter
  @NoArgsConstructor(access = AccessLevel.PROTECTED)
  public class PostTag {

      @Id
      @Column(name = &quot;post_tag_id&quot;)
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;

      // 다대일 관계, 여러 PostTag가 하나의 Post에 연결 가능
      @ManyToOne(fetch = FetchType.LAZY)
      @JoinColumn(name = &quot;post_id&quot;, nullable = false)
      private Post post;

      // 다대일 관계, 여러 PostTag가 하나의 Tag에 연결 가능
      @ManyToOne(fetch = FetchType.LAZY)
      @JoinColumn(name = &quot;tag_id&quot;, nullable = false)
      private Tag tag;

      public PostTag(Post post, Tag tag) {
          this.post = post;
          this.tag = tag;
      }
  }
</code></pre>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 완벽 이해하기]]></title>
            <link>https://velog.io/@daun_jung/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@daun_jung/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 10 Jul 2025 13:53:40 GMT</pubDate>
            <description><![CDATA[<p>멋쟁이 사자처럼 13기 강의를 진행하며 만든 자료를 블로그에 공유합니다.</p>
<h2 id="🗂️-데이터베이스란">🗂️ 데이터베이스란?</h2>
<h3 id="📌-데이터">📌 데이터</h3>
<p>관찰의 결과로 나타난 정량적인 실제 값</p>
<h3 id="📌-정보">📌 정보</h3>
<p>데이터에 의미를 부여한 것</p>
<h3 id="📌-데이터베이스">📌 데이터베이스</h3>
<p>조직에 필요한 정보를 얻기 위해 <strong>논리적으로 연관된 데이터</strong>를 모아 구조적으로 통합해 놓은 것</p>
<blockquote>
<p>예시)</p>
<p><strong>에베레스트산의 높이</strong> → 데이터</p>
<p><strong>에베레스트산의 지리적 특성</strong> → 정보</p>
</blockquote>
<h2 id="⚙️-데이터베이스-시스템">⚙️ 데이터베이스 시스템</h2>
<h3 id="✅-dbms">✅ DBMS</h3>
<p>데이터를 저장하고 관리하는 <strong>소프트웨어</strong></p>
<p>(ex. MySQL, Oracle, MariaDB)</p>
<p>→ 엑셀처럼 보일 수 있지만 훨씬 더 체계적</p>
<h3 id="✅-데이터베이스">✅ 데이터베이스</h3>
<p>실제 데이터가 저장되는 <strong>하드디스크의 구조</strong></p>
<p>→ 여러 개의 테이블로 구성, DBMS가 관리</p>
<blockquote>
<p>🧊 비유:</p>
<p>DB는 냉장고 안 음식</p>
<p>DBMS는 냉장고 문 여닫는 사람</p>
</blockquote>
<h3 id="✅-데이터-모델">✅ 데이터 모델</h3>
<p>데이터를 어떻게 구조화하고 표현할지를 정해주는 <strong>설계도</strong></p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/bf687249-1790-425c-89a3-0a7f81026d60/image.png" alt=""></p>
<p>⇒ 부모 자식 관계처럼 위계 구조로 표현</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/d513869f-473e-4efd-bb29-19761f365c19/image.png" alt=""></p>
<p>⇒ 하나의 데이터가 여러 부모와 연결될 수 있는 그래프 구조</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/5b263e70-2d7c-43fd-85d5-e21f40012f9a/image.png" alt=""></p>
<p>⇒ 데이터를 객체 단위로 저장</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/90c1dc93-9f8f-4074-9253-8f131ffe560d/image.png" alt=""></p>
<p>⇒ 데이터를 테이블(행과 열) 형태로 저장하고 테이블 간에 공통 키로 연결하여 관리</p>
<p>⇒ 가장 널리 쓰이는 모델</p>
<h2 id="🧠-용어의-이해">🧠 용어의 이해</h2>
<h3 id="📌-릴레이션relation">📌 릴레이션(Relation)</h3>
<p>행과 열로 구성된 <strong>테이블,</strong> 실제 데이터를 담고 있는 테이블</p>
<blockquote>
<p>🔹 스키마 + 인스턴스로 구성</p>
</blockquote>
<ul>
<li>릴레이션의 특징<ol>
<li>속성은 단일 값을 가짐</li>
<li>속성을 서로 다른 이름을 가짐</li>
<li>한 속성의 값은 모두 같은 도메인 값을 가짐 (도메인 : 속성이 가질 수 있는 값의 집합)</li>
<li>속성의 순서는 상관없음</li>
<li>릴레이션 내의 중복된 투플은 허용하지 않음</li>
<li>투플의 순서는 상관없음</li>
</ol>
</li>
</ul>
<h3 id="📌-주요-용어">📌 주요 용어</h3>
<ul>
<li><strong>스키마</strong>: 테이블을 만들기 위한 설계도, 틀</li>
<li><strong>인스턴스</strong>: 실제 저장된 데이터 집합</li>
<li><strong>속성(열)</strong>: 하나의 항목</li>
<li><strong>투플(행)</strong>: 하나의 데이터 묶음</li>
</ul>
<blockquote>
<p><strong>스키마</strong>: 쿠키 틀
<strong>인스턴스</strong>: 쿠키 완성품
<strong>속성</strong>: 쿠키의 맛, 모양, 재료
<strong>튜플</strong>: 개별 쿠키 하나</p>
</blockquote>
<h2 id="🔑-키-key">🔑 키 (Key)</h2>
<p>테이블에서 각 행(투플)을 <strong>유일하게 식별</strong>하거나</p>
<p>테이블 간 관계를 만들기 위해 사용하는 속성(열)을 의미함</p>
<hr>
<h3 id="✅-키의-두-가지-성질">✅ 키의 두 가지 성질</h3>
<ul>
<li><strong>유일성</strong>: 하나의 키 값으로 한 행(투플)을 <strong>유일하게 식별</strong>할 수 있어야 함</li>
<li><strong>최소성</strong>: 키를 구성하는 속성 중 <strong>꼭 필요한 최소한의 속성만</strong> 사용해야 함</li>
</ul>
<hr>
<h3 id="📌-키의-종류">📌 키의 종류</h3>
<h3 id="🔹-슈퍼키-super-key">🔹 슈퍼키 (Super Key)</h3>
<ul>
<li>행을 유일하게 식별할 수 있는 <strong>모든 후보 속성의 조합</strong></li>
<li>예: 주민번호, 주문번호, (id + 이름)</li>
<li><code>id + 이름</code>이 유일하더라도 <code>id</code>만으로도 유일하다면 <strong>후보키 아님</strong></li>
</ul>
<hr>
<h3 id="🔹-후보키-candidate-key">🔹 후보키 (Candidate Key)</h3>
<ul>
<li>슈퍼키 중 <strong>불필요한 속성을 제거한 최소 키</strong></li>
<li>예: 주민번호, 이메일, id</li>
</ul>
<hr>
<h3 id="🔹-기본키-primary-key-pk">🔹 기본키 (Primary Key, PK)</h3>
<ul>
<li>후보키 중 <strong>대표로 선택된 키</strong></li>
<li>후보키가 하나라면 → 그게 기본키</li>
<li>여러 개라면 → <strong>릴레이션 특성에 따라 선택</strong></li>
</ul>
<blockquote>
<p>💡 테이블 정의 시, 기본키는 밑줄로 표현하기도 함</p>
</blockquote>
<p>예시)
student (이름, 학번, 학과, 성별, 나이, 핸드폰)</p>
<ul>
<li>슈퍼키: 학번, 핸드폰, (이름 + 학번) …</li>
<li>후보키: 학번, 핸드폰</li>
<li>기본키: 학번</li>
</ul>
<hr>
<h3 id="🔹-대체키-alternate-key">🔹 대체키 (Alternate Key)</h3>
<ul>
<li>기본키로 선택되지 않은 <strong>나머지 후보키</strong></li>
</ul>
<hr>
<h3 id="🔹-대리키-surrogate-key">🔹 대리키 (Surrogate Key)</h3>
<ul>
<li>실제 데이터와 관계없는 <strong>인공적인 식별자</strong></li>
<li>예: 학번, 회원번호 (개인정보 유출 방지를 위해 주민번호 대신 사용)</li>
</ul>
<hr>
<h3 id="🔹-외래키-foreign-key-fk">🔹 외래키 (Foreign Key, FK)</h3>
<ul>
<li><strong>다른 테이블의 기본키를 참조</strong>하여 테이블 간 관계를 설정</li>
<li>외래키는 보통 자식 테이블에서 부모 테이블의 기본키를 가리킴</li>
</ul>
<hr>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/74b12eec-db91-43cd-ab7b-3e5ab009beea/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/5094e1a0-25d1-449a-a656-095b99fc1321/image.png" alt=""></p>
<p>주문 테이블의 기본키 : 주문번호</p>
<p>주문 테이블의 외래키 : </p>
<ul>
<li>고객번호 (고객 테이블 참조)</li>
<li>도서번호 (도서 테이블 참조)</li>
</ul>
<h2 id="🔗-테이블의-관계">🔗 테이블의 관계</h2>
<hr>
<h3 id="1️⃣-1--1-일대일-관계">1️⃣ 1 : 1 (일대일 관계)</h3>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/97d165e4-e24e-444f-96d9-3c82560f9815/image.png" alt=""></p>
<ul>
<li>하나의 데이터 ↔ 하나의 데이터만 연결됨</li>
<li><strong>ex)</strong> 각 전화번호는 단 하나의 사용자와만 연결됨</li>
</ul>
<hr>
<h3 id="2️⃣-1--n-일대다-관계">2️⃣ 1 : N (일대다 관계)</h3>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/526f823f-1859-4767-8ec6-843f682cb2e4/image.png" alt=""></p>
<ul>
<li>하나의 데이터가 다른 테이블의 여러 데이터와 연결됨</li>
<li>ex) 한 명은 여러개의 전화번호를 가질 수 있지만 여러 사람이 동일한 전화번호를 가질 수는 없음</li>
</ul>
<hr>
<h3 id="3️⃣-m--n-다대다-관계">3️⃣ M : N (다대다 관계)</h3>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/7df02a56-103a-48b9-8b13-aecc4ad95c39/image.png" alt=""></p>
<ul>
<li>여러 데이터가 서로 여러 개와 연결됨</li>
<li>ex) 한 명이 여러개의 패키지를 이용할 수도, 한개의 패키지를 여러명이 이용할 수도 있음</li>
</ul>
<hr>
<h3 id="4️⃣-자기참조-관계-self-join">4️⃣ 자기참조 관계 (Self-Join)</h3>
<p>하나의 테이블 내에서 관계를 표현</p>
<p>ex) 특정 서비스의 회원가입 시 추천인 id를 입력하는 기능</p>
<p> 자기 자신을 외래키로 참조해야 함 (ex. member.recommender_id → member.id)</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/fa73c897-0722-4e7e-acd0-79a8bf417140/image.png" alt=""></p>
<hr>
<h2 id="🧾-기본-sql-문법">🧾 기본 SQL 문법</h2>
<p>관계형 데이터베이스에서 데이터를 <strong>조회, 삽입, 수정, 삭제</strong>할 때 사용하는 기본 SQL 문법</p>
<hr>
<h3 id="🔍-select-조회">🔍 SELECT (조회)</h3>
<pre><code class="language-sql">SELECT 컬럼명 FROM 테이블명;</code></pre>
<ul>
<li>테이블에서 특정 컬럼을 조회</li>
<li><ul>
<li>를 사용하면 모든 컬럼 조회</li>
</ul>
</li>
</ul>
<hr>
<h3 id="🧩-where-조건-조회">🧩 WHERE (조건 조회)</h3>
<pre><code class="language-sql">
SELECT 컬럼명 FROM 테이블명 WHERE 조건;</code></pre>
<ul>
<li>조건이 참인 데이터만 조회</li>
</ul>
<hr>
<h3 id="🔃-order-by-정렬">🔃 ORDER BY (정렬)</h3>
<pre><code class="language-sql">SELECT 컬럼명 FROM 테이블명 WHERE 조건 ORDER BY 컬럼명 ASC;
SELECT 컬럼명 FROM 테이블명 WHERE 조건 ORDER BY 컬럼명 DESC;</code></pre>
<ul>
<li>불러오는 데이터를 정렬</li>
<li>ASC : 오름차순 (기본값)</li>
<li>DESC : 내림차순</li>
</ul>
<hr>
<h3 id="➕-insert-데이터-삽입">➕ INSERT (데이터 삽입)</h3>
<pre><code class="language-sql">INSERT INTO 테이블명 (칼럼명1, 칼럼명2, 칼럼명3) VALUES (값1, 값2, 값3)
INSERT INTO 테이블명 VALUES (값1, 값2, 값3)</code></pre>
<ul>
<li>입력 순서가 중요 (컬럼 순서와 값 순서 일치해야 함)</li>
</ul>
<hr>
<h3 id="🛠-update-데이터-수정">🛠 UPDATE (데이터 수정)</h3>
<pre><code class="language-sql">UPDATE 테이블명 SET 칼럼명 = 변경할 값                  //데이터의 모든 값을 변경
UPDATE 테이블명 SET 칼럼명 = 변경할 값 WHERE 조건       //조건에 해당하는 데이터만 변경</code></pre>
<ul>
<li>특정 행만 수정할 때는 반드시 where 조건 포함!</li>
</ul>
<hr>
<h3 id="❌-delete-데이터-삭제">❌ DELETE (데이터 삭제)</h3>
<pre><code class="language-sql">DELETE FROM 테이블명                  //데이터의 모든 값을 삭제
DELETE FROM 테이블명 WHERE 조건       //조건에 해당하는 데이터만 삭제</code></pre>
<ul>
<li>where 없이 DELETE를 쓰면 전체 데이터가 삭제되므로 주의!</li>
</ul>
<p>더 공부해보기!!</p>
<hr>
<h1 id="실습">!!실습!!</h1>
<h2 id="mysql-설치">MySql 설치</h2>
<p><a href="https://code-angie.tistory.com/158">https://code-angie.tistory.com/158</a></p>
<p><a href="https://velog.io/@cyseok123/MySQL-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%8B%A4%ED%96%89-for-Mac">https://velog.io/@cyseok123/MySQL-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%8B%A4%ED%96%89-for-Mac</a></p>
<p>모두 설치 완료해 오셨죵~?</p>
<h2 id="mysql-접속">MySql 접속</h2>
<ul>
<li>Mysql 접속 후 하단의 localhost:3306 DB서버 버튼 누르기</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/fe22c396-dd52-4a55-92da-d867e2a4f319/image.png" alt=""></p>
<ul>
<li>설치할 때 작성한 비밀번호 입력</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/0e945ed9-1d30-44d9-9591-30daf67d653a/image.png" alt=""></p>
<ul>
<li>Navigator : 어떤 데이터베이스(스키마)들이 있는지, Quary : 쿼리문 작성하는 곳, Output : 요청에 대한 답변 또는 실행 과정의 로그 메시지들</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/352f1118-da71-49a4-90e9-d2ed774d1575/image.png" alt=""></p>
<h2 id="데이터베이스-생성">데이터베이스 생성</h2>
<ul>
<li><p>데이터베이스를 생성해야함</p>
<p>  데이터베이스 → 테이블 → colum과 row들</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/ee19f0f1-1e6c-435f-a623-f92f92079d4f/image.png" alt=""></p>
<ul>
<li>데이터베이스 이름은 LikeLion, charset은 utf8, Collation은 데이터베이스 안에 들어있는 문자열등을 정렬하는 순서 utf8_genenal_ci</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/674c2a7a-fdae-429e-933d-1e592b83026c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/a0e82546-4057-4785-bbe7-2fc20f9dfbcc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/4981f54a-c4c2-4e67-a0f0-08c1fe9ee51f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/e9f5e7c0-17cd-4846-9fbd-b9e9ddcef3d3/image.png" alt=""></p>
<ul>
<li>원하는 데이터베이스 클릭 후 SQL 아이콘 클릭</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/61f89972-7ff7-467d-99a1-fe42d9c1c7ea/image.png" alt=""></p>
<h2 id="테이블-생성">테이블 생성</h2>
<pre><code class="language-sql">USE LikeLion; -- 사용할 데이터베이스 선택

CREATE TABLE teams (
        id INTEGER PRIMARY KEY,  -- 팀(파트) 고유 ID, 기본키, 정수값
    name VARCHAR(50) NOT NULL  -- 팀(파트) 이름 (Backend, Frontend, AI), 글자로 최대 50자, null값 안됨
);

CREATE TABLE users (
        id INTEGER AUTO_INCREMENT PRIMARY KEY,  -- 사용자 고유 ID, 자동으로 1씩 증가, 기본키
    name VARCHAR(50) NOT NULL,  -- 사용자 이름, 글자로 최대 50자, null값 안됨
    part_id INTEGER,  -- 소속된 파트 ID teams 테이블의 id 참조, 정수값
    FOREIGN KEY (part_id) REFERENCES teams(id)  -- 외래키 : part_id는 teams 테이블의 id와 연결
);</code></pre>
<p>→ 외래키를 통해 users.part_id는 반드시 teams.id에 존재하는 값만 가질 수 있다</p>
<p>→ teams 테이블의 id가 부모 키, users.part_id가 자식 키</p>
<p>→ 이를 통해 part_id에 존재하지 않는 팀 id를 입력하면 오류</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/abf09b31-b068-49bf-8bf3-7d92aae59547/image.png" alt=""></p>
<ul>
<li>쿼리문 작성 후 번개 아이콘 클릭, 새로고침하면 테이블을 확인할 수 있다</li>
</ul>
<h2 id="데이터-입력">데이터 입력</h2>
<pre><code class="language-sql">USE LikeLion;

INSERT INTO teams (id, name) VALUES
(1, &#39;Backend&#39;),
(2, &#39;Frontend&#39;),
(3, &#39;AI&#39;);</code></pre>
<ul>
<li>users 테이블에서 part_id를 참조 해야 하기 때문에 먼저 팀 정보부터 입력</li>
</ul>
<pre><code class="language-sql">
USE LikeLion;

INSERT INTO users (name, part_id) VALUES
(&#39;다운&#39;, 1),
(&#39;준영&#39;, 1),
(&#39;하윤&#39;, 2),
(&#39;현승&#39;, 2),
(&#39;현민&#39;, 3),
(&#39;규빈&#39;, 3);</code></pre>
<ul>
<li>users 데이터 입력</li>
<li>여기서 users에 id 값을 채워넣지 않아도 되는 이유 : AUTO_INCREMENT로 자동으로 1부터 순서대로 증가시켜 채워준다</li>
</ul>
<h2 id="조회">조회</h2>
<ul>
<li>전체 컬럼 조회를 하기 위해 *를 사용</li>
</ul>
<pre><code class="language-sql">SELECT * FROM teams;  -- teams 테이블 조회</code></pre>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/0b5ca1ba-deb4-41e9-ab0e-c6e2e36a5f24/image.png" alt=""></p>
<pre><code class="language-sql">SELECT * FROM users;  -- users 테이블 조회</code></pre>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/193157c3-01e8-4505-a8d9-2519e8cc53e0/image.png" alt=""></p>
<ul>
<li>특정 컬럼만 조회하고 싶거나 조건을 걸어서 조회하고 싶다면 아까 배운 sql문법에 따라 시도해 보아요~</li>
</ul>
<h2 id="수정-삭제">수정, 삭제</h2>
<h3 id="수정">수정</h3>
<ul>
<li>아이디가 n인 사용자(다운)의 팀을 AI(3번)으로 변경</li>
</ul>
<pre><code class="language-sql">USE LikeLion;

UPDATE users
SET part_id = 3
WHERE id = (위 테이블을 보고 이름 다운의 id값을 넣어주세용..);</code></pre>
<ul>
<li>다시 조회</li>
</ul>
<pre><code class="language-sql">SELECT * FROM users;</code></pre>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/b0455005-436f-4de7-9764-fd9b10e2cd9e/image.png" alt=""></p>
<h3 id="삭제">삭제</h3>
<ul>
<li>삭제 전 조건이 맞는 행이 있는지 먼저 확인하는 습관이 좋음</li>
</ul>
<pre><code class="language-sql">SELECT * FROM users WHERE id = (위 테이블을 보고 이름 다운의 id값을 넣어주세용..);</code></pre>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/b13486af-c897-4716-814b-ca75967d3128/image.png" alt=""></p>
<pre><code class="language-sql">DELETE FROM users 
WHERE id = (위 테이블을 보고 이름 다운의 id값을 넣어주세용..);</code></pre>
<pre><code class="language-sql">SELECT * FROM users; -- 조회</code></pre>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/715dab7f-f9da-4ee9-bc68-0b1d28e6affc/image.png" alt=""></p>
<h2 id="🔗-join">🔗 JOIN</h2>
<p>두 개 이상의 테이블을 <strong>공통된 컬럼을 기준으로 연결</strong>하여 데이터를 조회할 때 사용</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/3c087dc5-2d11-40d6-a7b3-6514ed9a6988/image.png" alt=""></p>
<h3 id="🔸-inner-join-내부-조인">🔸 INNER JOIN (내부 조인)</h3>
<ul>
<li>내부조인 → 교집합</li>
<li>기준 테이블과 JOIN 테이블의 중복된 값을 보여줌</li>
<li>두 테이블에 모두 지정한 열의 데이터가 있어야함</li>
<li>가장 자주 사용되는 JOIN</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/cfa6340d-b4ed-4680-b6d8-240d4e1d1e57/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/d281fe7f-2a18-4863-9284-e15888becb5a/image.png" alt=""></p>
<h3 id="🔹-left-outer-join">🔹 LEFT (OUTER) JOIN</h3>
<ul>
<li>부분집합</li>
<li>공통 부분과 왼쪽 테이블 데이터 (왼쪽에 있는 테이블 모두 보여줌)</li>
<li>연결된 데이터가 없으면 NULL로 표시</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/f1676af5-c30d-434a-8f3f-de970f2746c0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/867bc3fa-0275-4a3c-88a0-76660463b0ce/image.png" alt=""></p>
<h3 id="🔸-right-outer-join">🔸 RIGHT (OUTER) JOIN</h3>
<ul>
<li>부분집합</li>
<li>공통 부분과 오른쪽 테이블 데이터 (오른쪽에 있는 테이블 모두 보여줌)</li>
<li>기준 테이블에 연결된 값이 없으면 NULL로 표시</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/606dcf99-0fd7-4d1c-b572-6f013a652db7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/4776a424-bf32-4278-a198-48fee432d5c4/image.png" alt=""></p>
<h3 id="🔸-full-outer-join">🔸 FULL OUTER JOIN</h3>
<ul>
<li>외부조인 → 합집합</li>
<li>A, B 테이블 데이터 모두를 보여줌</li>
<li>연결되지 않은 값은 NULL로 표시</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/abee276e-0511-4885-be02-227b66c7e9b0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/dba4ee0d-f952-4376-88a6-b192d94cf834/image.png" alt=""></p>
<h3 id="inner-join-실습">INNER JOIN 실습</h3>
<ul>
<li>실습을 위해 어떤 팀과도 연결되지 않은 사용자를 추가</li>
</ul>
<pre><code class="language-sql">INSERT INTO users (name, part_id)
VALUES (&#39;멋사&#39;, NULL);</code></pre>
<ul>
<li>조회</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/45fb1e28-c580-4373-89e1-067887dbdcda/image.png" alt=""></p>
<ul>
<li>INNER JOIN으로 쿼리를 작성</li>
</ul>
<pre><code class="language-sql">SELECT
    users.name AS 사용자명,  -- AS로 열 이름을 사용자명으로 바꿔서 표시 
  teams.name AS 소속팀
FROM  -- 기준이 되는 메인 테이블 지정
    users
JOIN  -- teams테이블과 연결
    teams
ON  
-- 어떤 열을 기준으로 연결할지 설정, users의 part_id와 teams의 id가 일치할 때만 데이터를 합침
    users.part_id = teams.id;</code></pre>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/b7dbbfcf-e9ce-4fd9-93e5-ffb25c9298a6/image.png" alt=""></p>
<p>⇒ 사용자 이름과 소속 팀 이름을 각각 다른 테이블에서 가져와서 한 화면에 보여주는 join 예제</p>
<p>⇒ JOIN을 통해 관계형 데이터베이스의 연결된 구조를 직접 조회할 수 있다</p>
<p>⇒ JOIN 전에는 팀이 없는 ‘멋사’ 가 테이블에 표시, JOIN 후에는 조건에 맞는 데이터가 없기 때문에 ‘멋사’가 테이블에 표시되지 않음</p>
<h2 id="🗺️-erd-entity-relationship-diagram">🗺️ ERD (Entity Relationship Diagram)</h2>
<blockquote>
<p>ERD는 Entity(개체), Relationship(관계), Attribute(속성) 간의</p>
<p>구조를 <strong>그림으로 시각화</strong>한 데이터베이스 설계 도구</p>
</blockquote>
<h3 id="erd의-핵심-3요소">ERD의 핵심 3요소</h3>
<h3 id="🧱-1-entity-개체">🧱 1. Entity (개체)</h3>
<ul>
<li>엔티티는 정의 가능한 사물 또는 개념을 의미</li>
<li>데이터베이스의 테이블이 엔티티로 표현됨</li>
<li>예 : 사용자, 게시글, 상품</li>
</ul>
<h3 id="🔗-2-relationship-관계">🔗 2. Relationship (관계)</h3>
<ul>
<li>개체 사이의 연관성을 나타내는 개념</li>
<li>ERD에서는 선으로 두 개체를 연결</li>
<li>1:1 관계, 1:N 관계, N:M 관계</li>
</ul>
<h3 id="📌-3-attribute-속성">📌 3. Attribute (속성)</h3>
<ul>
<li>하나의 엔티티가 가지는 정보 요소</li>
<li>데이터베이스에서는 컬럼으로 표현</li>
<li>예 : id, name, email</li>
</ul>
<blockquote>
<p>💡 정리</p>
<p>ERD는 개체(Entity)가 어떤 속성(Attribute)을 가지고,</p>
<p>다른 개체와 어떤 관계(Relationship)를 맺는지</p>
<p><strong>시각적으로 표현해주는 설계 도구</strong></p>
</blockquote>
<h2 id="erd-cloud-실습">ERD CLOUD 실습</h2>
<p><a href="https://www.erdcloud.com/">ERDCloud</a></p>
<ul>
<li>사이트에 접속 후 사용 버튼 클릭</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/75ecdac0-56c0-4db2-bab1-083d4fde56c5/image.png" alt=""></p>
<ul>
<li>회원가입 창에서 로그인 해주기</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/29499b44-6e4e-4643-8fec-d15f5cc6bbb4/image.png" alt=""></p>
<ul>
<li>로그인 후 제목 작성 후 만들기</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/b092ac30-de7e-445e-9bc0-6ae467910d09/image.png" alt=""></p>
<ul>
<li>ERD 설정을 해주기 위해 톱니바퀴 클릭</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/009993ce-518f-4a53-9968-8b0556249fe8/image.png" alt=""></p>
<ul>
<li>디스플레이 항목에 모두 체크해주기 참고로 검은색 사각형에 하얀 테두리가 체크가 된 상태!!</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/d5e63262-a1c6-470f-9f22-61445ba4a2f8/image.png" alt=""></p>
<h3 id="erd-다이어그램-그리기">ERD 다이어그램 그리기</h3>
<ul>
<li>왼쪽 메뉴바에 엔티티 추가 버튼 누르고 빈 영역에 클릭하면 엔티티 생성</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/1ca2c54b-4417-428a-8450-caaa32c6819d/image.png" alt=""></p>
<ul>
<li>키 생성 버튼과 필드 생성 버튼을 한번씩 누르면 다음과 같은 테이블이 완성!</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/162364c6-cde1-4986-8ee5-7c8e772e1017/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/ff73459f-0b60-4383-a773-a506720a8eb6/image.png" alt=""></p>
<ul>
<li>예시로 완성된 회원정보와 게시글 테이블</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/204cdf91-25a2-47c3-859e-5f0ce61446e5/image.png" alt=""></p>
<p>→ 시간상의 이유로 회원정보의 id, 게시글의 id만 필수적으로 생성해보기!</p>
<h3 id="erd-관계-맺기">ERD 관계 맺기</h3>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/d7be7cba-5712-426e-a110-c4e44df90e5a/image.png" alt=""></p>
<ul>
<li>위 회원정보와 게시글 테이블 관계 맺기</li>
<li>하나의 회원은 여러개의 게시글 작성 가능, 회원은 게시글을 하나도 작성하지 않을 수 있음</li>
<li>게시글은 한명의 회원만이 게시글을 쓸 수 있음, 반드시 회원 정보를 가지고 있어야 함</li>
<li>⇒ 회원정보(1) : 게시글(N)</li>
</ul>
<p>따라서 왼쪽 사이드 메뉴에서 알맞는 아이콘 클릭 후 회원정보 테이블, 게시글 테이블을 순서대로 클릭</p>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/92caf8c4-5acf-4dd0-a3ca-d09f4bc31e9f/image.png" alt=""></p>
<ul>
<li>Non-Identifying Relationship클릭 - 외래키가 자식 테이블의 기본키이지 않음 게시글 테이블에서 user_id는 외래키지만 기본키는 아님</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/513d9d61-9c87-4a02-8f71-11bbc208aaca/image.png" alt=""></p>
<ul>
<li>자동으로 외래키 레코드 삽입</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/b8810757-9def-4112-b550-528c70451f11/image.png" alt=""></p>
<ul>
<li>마지막으로 내보내기를 통해 쿼리문 추출도 가능</li>
</ul>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/7ce388e3-52b6-4ced-81ef-e42b0d10e817/image.png" alt=""></p>
<h1 id="🥳-끝-모두-수고-많으셨습니당-🦁">🥳 끝!! 모두 수고 많으셨습니당 ~~🦁</h1>
]]></description>
        </item>
        <item>
            <title><![CDATA[스트림 API1 - 기본]]></title>
            <link>https://velog.io/@daun_jung/%EC%8A%A4%ED%8A%B8%EB%A6%BC-API1-%EA%B8%B0%EB%B3%B8</link>
            <guid>https://velog.io/@daun_jung/%EC%8A%A4%ED%8A%B8%EB%A6%BC-API1-%EA%B8%B0%EB%B3%B8</guid>
            <pubDate>Mon, 02 Jun 2025 03:41:35 GMT</pubDate>
            <description><![CDATA[<h3 id="스트림-api-개요">스트림 API 개요</h3>
<p>스트림(Stream)은 자바 8부터 도입된 기능으로, 데이터의 흐름을 추상화하여 컬렉션이나 배열의 요소들을 선언형 방식으로 처리할 수 있게 해줍니다. 우리가 앞서 MyStreamV3를 구현하며 필터와 맵 등의 기능을 익힌 것처럼, 스트림 API는 데이터가 마치 물처럼 흐르듯 여러 연산을 거쳐 최종 결과를 만들어냅니다.</p>
<pre><code class="language-java">List&lt;String&gt; result = students.stream()
    .filter(s -&gt; s.getScore() &gt;= 80)
    .map(s -&gt; s.getName())
    .toList();</code></pre>
<p>이처럼 <strong>무엇을 할지(What)</strong>에 집중하는 선언형 프로그래밍을 가능하게 합니다.</p>
<h3 id="스트림의-핵심-구성-요소">스트림의 핵심 구성 요소</h3>
<ul>
<li><p>중간 연산(Intermediate Operation): filter, map 등. 데이터를 걸러내거나 변환합니다.</p>
</li>
<li><p>최종 연산(Terminal Operation): toList, forEach, findFirst 등. 실제 실행을 유발하며 스트림이 종료됩니다.</p>
</li>
<li><p>내부 반복(Internal Iteration): 루프를 명시하지 않고 스트림 내부에서 반복합니다.</p>
</li>
<li><p>메서드 참조(Method Reference): String::toUpperCase, System.out::println과 같이 람다식을 더 간결하게 표현합니다.</p>
</li>
</ul>
<h3 id="t스트림의-특징">t스트림의 특징</h3>
<ul>
<li><p>불변성(Immutable): 원본 데이터는 변경되지 않음</p>
</li>
<li><p>1회성(Consumed Once): 스트림은 한 번 사용하면 재사용 불가</p>
</li>
<li><p>지연 연산(Lazy Evaluation): 최종 연산이 실행되기 전까지 중간 연산은 실행되지 않음</p>
</li>
<li><p>파이프라인 구성(Pipelining): 연산들이 체이닝되어 요소 하나 단위로 흐름 처리됨</p>
</li>
<li><p>병렬 처리 지원: parallelStream()으로 병렬 연산 수행 가능</p>
</li>
</ul>
<h3 id="일괄-처리-vs-파이프라인-처리">일괄 처리 vs 파이프라인 처리</h3>
<p>MyStreamV3: 일괄 처리 방식. 필터를 전체에 적용 후 결과 모아서 맵 수행.</p>
<p>Stream API: 파이프라인 처리 방식. 각 요소가 필터를 통과하면 바로 다음 연산으로 이어짐.</p>
<p>예시:</p>
<pre><code class="language-java">List&lt;Integer&gt; result = data.stream()
    .filter(i -&gt; i % 2 == 0)
    .map(i -&gt; i * 10)
    .toList();</code></pre>
<p>1 → X, 2 → 필터 통과 → map(2*10)처럼 하나의 요소가 파이프라인을 통과하듯 처리됨.</p>
<h3 id="지연-연산과-최적화">지연 연산과 최적화</h3>
<ul>
<li><p>즉시 연산: MyStreamV3는 filter나 map 호출 시 바로 실행됨</p>
</li>
<li><p>지연 연산: Stream API는 toList(), forEach() 등의 최종 연산이 호출되기 전까지 실제 연산 수행 안함</p>
</li>
</ul>
<pre><code class="language-java">data.stream()
    .filter(...)
    .map(...)
    .findFirst().get(); // 이 순간에만 실행됨</code></pre>
<p>→ 이런 방식 덕분에 단축 평가(Short-circuiting) 가능: findFirst는 조건을 만족하는 첫 값을 찾자마자 나머지 연산 생략</p>
<h3 id="정리">정리</h3>
<p>선언형 프로그래밍을 가능하게 함</p>
<p>불필요한 연산 줄이기 위한 지연 연산과 단축 평가 제공</p>
<p>스트림은 복잡한 데이터 처리 로직을 간결하게 만들고, 메모리 사용과 성능 면에서 효율적</p>
<p>MyStreamV3처럼 직접 구현한 방식과 비교하며 스트림 API의 장점을 체감할 수 있음</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[매서드 참조]]></title>
            <link>https://velog.io/@daun_jung/%EB%A7%A4%EC%84%9C%EB%93%9C-%EC%B0%B8%EC%A1%B0</link>
            <guid>https://velog.io/@daun_jung/%EB%A7%A4%EC%84%9C%EB%93%9C-%EC%B0%B8%EC%A1%B0</guid>
            <pubDate>Mon, 26 May 2025 03:28:42 GMT</pubDate>
            <description><![CDATA[<h2 id="자바-메서드-참조method-reference-정리">자바 메서드 참조(Method Reference) 정리</h2>
<p>람다식으로 코드를 간결하게 만들 수 있지만, 이미 정의된 메서드를 단순히 호출만 하는 경우에는 굳이 람다로 감싸는 게 오히려 장황해질 수 있다. 이럴 때 사용할 수 있는 게 바로 <strong>메서드 참조(Method Reference)</strong>이다.</p>
<p>람다를 좀 더 간단하게 표현할 수 있는 문법적인 축약형이라고 보면 된다.</p>
<p>메서드 참조가 필요한 이유
기본적인 덧셈 연산을 람다식으로 표현하면 다음과 같다.</p>
<pre><code>BinaryOperator&lt;Integer&gt; add1 = (x, y) -&gt; x + y;</code></pre><p>이걸 조금 개선해서, 별도의 add() 메서드를 호출하게 만들면 중복은 줄어들지만 여전히 람다 구문은 길다.</p>
<pre><code>BinaryOperator&lt;Integer&gt; add2 = (x, y) -&gt; add(x, y);</code></pre><p>이 경우 메서드 참조를 쓰면 아래처럼 훨씬 간단해진다.</p>
<pre><code>BinaryOperator&lt;Integer&gt; add3 = ClassName::add;
</code></pre><p>(x, y) -&gt; add(x, y)와 같은 람다는 ClassName::add로 줄일 수 있다.</p>
<h2 id="메서드-참조의-4가지-유형">메서드 참조의 4가지 유형</h2>
<p>정적 메서드 참조</p>
<p>클래스명::정적메서드명</p>
<p>예: Math::max, Integer::parseInt</p>
<p>특정 객체의 인스턴스 메서드 참조</p>
<p>객체명::인스턴스메서드명</p>
<p>예: person::introduce</p>
<p>생성자 참조</p>
<p>클래스명::new</p>
<p>예: Person::new</p>
<p>임의 객체의 인스턴스 메서드 참조</p>
<p>클래스명::인스턴스메서드명</p>
<p>예: Person::introduce</p>
<h3 id="예제별-정리">예제별 정리</h3>
<ol>
<li>정적 메서드 참조<pre><code>Supplier&lt;String&gt; s1 = () -&gt; Person.greeting();
Supplier&lt;String&gt; s2 = Person::greeting;</code></pre></li>
<li>특정 객체의 인스턴스 메서드 참조<pre><code>Person p = new Person(&quot;Kim&quot;);
Supplier&lt;String&gt; s1 = () -&gt; p.introduce();
Supplier&lt;String&gt; s2 = p::introduce;</code></pre></li>
<li>생성자 참조<pre><code>Supplier&lt;Person&gt; s1 = () -&gt; new Person();
Supplier&lt;Person&gt; s2 = Person::new;</code></pre></li>
<li>임의 객체의 인스턴스 메서드 참조<pre><code>Function&lt;Person, String&gt; f1 = p -&gt; p.introduce();
Function&lt;Person, String&gt; f2 = Person::introduce;</code></pre>이때 Person::introduce는 Function&lt;Person, String&gt; 타입에서 apply()의 인자로 넘어온 Person 객체가 해당 메서드를 호출하게 된다.</li>
</ol>
<h3 id="활용-예시">활용 예시</h3>
<p>리스트 처리</p>
<pre><code>List&lt;Person&gt; people = List.of(new Person(&quot;Kim&quot;), new Person(&quot;Lee&quot;));
List&lt;String&gt; result = people.stream()
                            .map(Person::introduce)
                            .collect(Collectors.toList());</code></pre><p>문자열 변환</p>
<pre><code>List&lt;String&gt; upper = strings.stream()
                            .map(String::toUpperCase)
                            .collect(Collectors.toList());</code></pre><p>매개변수가 있을 때
메서드 참조는 매개변수가 있어도 잘 동작한다. 예를 들어 다음과 같이 Function&lt;String, String&gt;을 만들 때도 사용 가능하다.</p>
<pre><code>Function&lt;String, String&gt; f1 = name -&gt; Person.greetingWithName(name);
Function&lt;String, String&gt; f2 = Person::greetingWithName;</code></pre><p>이처럼 람다가 단순히 메서드 호출만 하고 있다면 메서드 참조로 충분히 대체 가능하다.</p>
<h3 id="메서드-참조의-장점-정리">메서드 참조의 장점 정리</h3>
<ul>
<li><p>코드가 간결해진다.</p>
</li>
<li><p>가독성이 좋아진다.</p>
</li>
<li><p>재사용성이 높아진다.</p>
</li>
<li><p>특히 스트림 API와 함께 사용할 때 유용하다.</p>
</li>
</ul>
<p>마무리
람다로도 충분히 표현할 수 있는 코드라 하더라도, 호출하는 메서드만 있는 경우엔 메서드 참조가 훨씬 깔끔하다. 자바에서 함수형 인터페이스와 람다를 사용할 일이 많아지는 요즘, 메서드 참조는 익혀두면 무조건 이득인 문법이다. 익숙해지면 쓸 수 있는 곳이 꽤 많다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[람다 VS 익명 클래스]]></title>
            <link>https://velog.io/@daun_jung/%EB%9E%8C%EB%8B%A4-VS-%EC%9D%B5%EB%AA%85-%ED%81%B4%EB%9E%98%EC%8A%A4</link>
            <guid>https://velog.io/@daun_jung/%EB%9E%8C%EB%8B%A4-VS-%EC%9D%B5%EB%AA%85-%ED%81%B4%EB%9E%98%EC%8A%A4</guid>
            <pubDate>Sat, 17 May 2025 11:55:30 GMT</pubDate>
            <description><![CDATA[<h1 id="람다-vs-익명-클래스-무엇이-다를까">람다 VS 익명 클래스 무엇이 다를까?</h1>
<p>자바 개발을 하다 보면 익명 클래스와 람다 표현식 중 어떤 걸 써야 할지 고민될 때가 있죠.
두 문법은 비슷해 보이지만, 의외로 차이가 뚜렷합니다.
이번 포스팅에서는 자바의 두 대표 단축 문법인 <strong>람다(Lambda)</strong>와 <strong>익명 클래스(Anonymous Class)</strong>를 비교해보겠습니다.</p>
<h2 id="1-문법-차이">1. 문법 차이</h2>
<h4 id="익명-클래스">익명 클래스</h4>
<pre><code class="language-java">button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        System.out.println(&quot;클릭!&quot;);
    }
});
</code></pre>
<p>new 인터페이스명()으로 바로 객체 생성</p>
<p>메서드 오버라이딩 필요</p>
<p>문법이 길다</p>
<h4 id="람다-표현식">람다 표현식</h4>
<pre><code class="language-java">button.setOnClickListener(v -&gt; System.out.println(&quot;클릭!&quot;));</code></pre>
<p>-&gt; 연산자 사용</p>
<p>함수형 인터페이스에만 사용 가능</p>
<p>짧고 가독성이 좋음</p>
<h2 id="2-코드의-간결함">2. 코드의 간결함</h2>
<p>람다는 명확히 간결하고 직관적입니다.
반면, 익명 클래스는 코드가 장황하고 가독성이 떨어질 수 있습니다.</p>
<h2 id="3-this의-의미">3. this의 의미</h2>
<p>구분    this 키워드가 가리키는 대상
익명 클래스    익명 클래스 자신
람다    람다를 선언한 외부 클래스</p>
<h2 id="4-상속과-상태-관리">4. 상속과 상태 관리</h2>
<h4 id="익명-클래스-1">익명 클래스</h4>
<p>필드나 메서드를 가질 수 있음</p>
<p>상태를 유지 가능</p>
<p>여러 메서드를 가진 인터페이스 구현 가능</p>
<h4 id="람다">람다</h4>
<p>함수형 인터페이스만 사용 가능 (메서드 1개)</p>
<p>필드나 상태 없음 (순수 함수 느낌)</p>
<h2 id="5-호환성--동작-원리">5. 호환성 &amp; 동작 원리</h2>
<p>구분    작동 방식
익명 클래스    컴파일 시 별도 클래스 파일 생성 (예: OuterClass$1.class)
람다    invokeDynamic 사용 → 런타임에 코드 연결</p>
<p>즉, 람다는 가볍고 동적, 익명 클래스는 정적이고 무겁다고 볼 수 있습니다.</p>
<h2 id="6-변수-캡처-규칙-capturing">6. 변수 캡처 규칙 (Capturing)</h2>
<p>둘 다 외부 지역 변수는 final 혹은 사실상 final(값 변경 안됨)만 접근 가능</p>
<p>바뀌는 값은 참조 불가</p>
<pre><code class="language-java">int a = 10; // OK
int b = 20; b++; // ❌ 캡처 불가</code></pre>
<h2 id="7-언제-어떤-걸-써야-할까">7. 언제 어떤 걸 써야 할까?</h2>
<p>상황    추천 문법
단순한 콜백 or 이벤트 리스너     람다
여러 메서드를 구현해야 할 때     익명 클래스
상태를 유지해야 할 때          익명 클래스
자바 8 이전 버전               익명 클래스</p>
<h3 id="마무리-요약">마무리 요약</h3>
<p><img src="https://velog.velcdn.com/images/daun_jung/post/f630f8ce-9775-443e-bf9a-045b9205463b/image.png" alt=""></p>
<h3 id="결론">결론</h3>
<p>람다는 자바 8의 대표적인 함수형 기능으로, 익명 클래스를 대체하는 현대적인 방식입니다.
다만 여전히 복잡한 인터페이스 구현이나 상태 유지가 필요한 상황에서는 익명 클래스가 유용할 수 있습니다.</p>
<p>상황에 따라 적절히 선택하면서 코드를 더 깔끔하고 효율적으로 만들어보세요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[람다 활용]]></title>
            <link>https://velog.io/@daun_jung/%EB%9E%8C%EB%8B%A4-%ED%91%9C%ED%98%84%EC%8B%9D%EA%B3%BC-%EC%8A%A4%ED%8A%B8%EB%A6%BC-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@daun_jung/%EB%9E%8C%EB%8B%A4-%ED%91%9C%ED%98%84%EC%8B%9D%EA%B3%BC-%EC%8A%A4%ED%8A%B8%EB%A6%BC-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 12 May 2025 14:39:30 GMT</pubDate>
            <description><![CDATA[<h1 id="람다-표현식과-스트림-사용법-정리">람다 표현식과 스트림 사용법 정리</h1>
<h2 id="1-명령형-vs-선언적-프로그래밍">1. 명령형 vs 선언적 프로그래밍</h2>
<h3 id="명령형-프로그래밍imperative-programming">명령형 프로그래밍(Imperative Programming)</h3>
<ul>
<li><strong>어떻게(How)</strong> 해결할지를 단계별로 명시</li>
<li>주로 <code>for</code>, <code>while</code>, <code>if</code> 등의 제어문을 사용하여 반복과 조건을 직접 제어</li>
<li>상태 변화를 명확히 관리하고, 코드의 흐름이 직관적이지만 복잡한 로직에서 중복 코드 발생 가능</li>
</ul>
<h3 id="선언적-프로그래밍declarative-programming">선언적 프로그래밍(Declarative Programming)</h3>
<ul>
<li>무엇(What)을 수행할지를 명시하여 코드의 의도를 명확히 표현</li>
<li>내부 구현은 숨기고 원하는 결과만을 나타냄으로써 코드의 간결성과 가독성을 높임</li>
<li>필터링, 매핑, 집계 등의 연산을 표현할 때 특히 효과적임</li>
</ul>
<h2 id="2-필터filter와-맵map">2. 필터(Filter)와 맵(Map)</h2>
<h3 id="필터">필터</h3>
<ul>
<li>데이터 중 조건에 맞는 값만을 선별하는 작업</li>
<li>주로 <code>Predicate</code>를 사용하며, boolean 값을 반환하는 함수형 인터페이스</li>
</ul>
<pre><code class="language-java">Predicate&lt;Integer&gt; evenPredicate = n -&gt; n % 2 == 0;
List&lt;Integer&gt; evenNumbers = numbers.stream()
                                   .filter(evenPredicate)
                                   .collect(Collectors.toList());</code></pre>
<h3 id="맵">맵</h3>
<ul>
<li>데이터를 다른 값으로 변환하는 작업</li>
<li>주로 <code>Function</code>을 사용하며, 입력값을 다른 타입이나 형태로 변환하는 역할 수행</li>
</ul>
<pre><code class="language-java">Function&lt;String, Integer&gt; lengthMapper = s -&gt; s.length();
List&lt;Integer&gt; lengths = strings.stream()
                               .map(lengthMapper)
                               .collect(Collectors.toList());</code></pre>
<h3 id="genericfilter와-genericmapper">GenericFilter와 GenericMapper</h3>
<ul>
<li>제네릭을 도입하여 다양한 데이터 타입에 대해 필터링과 매핑을 유연하게 적용 가능</li>
<li>코드 중복을 최소화하고 유지보수성을 높임</li>
</ul>
<pre><code class="language-java">List&lt;Integer&gt; filtered = GenericFilter.filter(numbers, n -&gt; n % 2 == 0);
List&lt;Integer&gt; mapped = GenericMapper.map(filtered, n -&gt; n * 2);</code></pre>
<h2 id="3-스트림stream">3. 스트림(Stream)</h2>
<h3 id="스트림-개념">스트림 개념</h3>
<ul>
<li>데이터를 흐름으로 보고 이를 연속적인 연산으로 처리할 수 있도록 지원하는 추상화된 개념</li>
<li>메서드 체인을 사용하여 간결하게 코드를 작성 가능</li>
</ul>
<h3 id="스트림의-장점">스트림의 장점</h3>
<ul>
<li>명확한 데이터 처리 흐름과 선언적인 코드로 가독성 향상</li>
<li>내부 반복을 통해 개발자는 반복의 구현 방식이 아닌 데이터 처리의 의도에 집중 가능</li>
</ul>
<h3 id="스트림-예시">스트림 예시</h3>
<pre><code class="language-java">List&lt;Integer&gt; result = MyStream.of(numbers)
    .filter(n -&gt; n % 2 == 0)
    .map(n -&gt; n * 2)
    .toList();</code></pre>
<h2 id="4-내부-반복-vs-외부-반복">4. 내부 반복 vs 외부 반복</h2>
<h3 id="외부-반복external-iteration">외부 반복(External Iteration)</h3>
<ul>
<li>개발자가 반복 제어 구조를 명시적으로 관리하며 데이터 순회</li>
<li>반복문 내에서 조건에 따라 흐름 제어(<code>break</code>, <code>continue</code>)가 쉽지만, 반복 구조가 복잡해지면 코드의 가독성이 떨어질 수 있음</li>
</ul>
<pre><code class="language-java">for (String s : list) {
    System.out.println(s);
}</code></pre>
<h3 id="내부-반복internal-iteration">내부 반복(Internal Iteration)</h3>
<ul>
<li>반복 제어가 스트림 내부에서 관리됨</li>
<li>개발자는 데이터 처리 방법(람다 표현식)에만 집중할 수 있음</li>
<li>코드의 가독성 및 유지보수성이 뛰어나며 선언적 프로그래밍 방식과 어울림</li>
</ul>
<pre><code class="language-java">list.stream().forEach(s -&gt; System.out.println(s));</code></pre>
<h2 id="5-정적-팩토리-메서드static-factory-method">5. 정적 팩토리 메서드(Static Factory Method)</h2>
<ul>
<li>객체 생성 방식을 메서드로 캡슐화하여 가독성을 높이고 생성자의 한계를 극복</li>
<li>객체 생성 시 의미있는 이름을 부여하여 객체 생성 로직을 명확히 나타낼 수 있음</li>
</ul>
<pre><code class="language-java">MyStream.of(numbers);</code></pre>
<ul>
<li>간단한 생성 로직에는 일반 생성자가 적합하지만, 복잡하거나 명확한 이름이 필요한 경우 정적 팩토리 메서드가 유리함</li>
</ul>
<h2 id="정리">정리</h2>
<p>람다 표현식과 스트림을 적극 활용하면 선언적 프로그래밍을 통해 더욱 간결하고 직관적인 코드를 작성할 수 있다. 명령형 프로그래밍은 세부적인 제어가 필요할 때 적합하며, 상황에 따라 두 방식을 적절히 활용하면 더욱 효율적인 프로그래밍이 가능하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[함수형 인터페이스]]></title>
            <link>https://velog.io/@daun_jung/%ED%95%A8%EC%88%98%ED%98%95-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4</link>
            <guid>https://velog.io/@daun_jung/%ED%95%A8%EC%88%98%ED%98%95-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4</guid>
            <pubDate>Sun, 04 May 2025 11:43:06 GMT</pubDate>
            <description><![CDATA[<h2 id="함수형-인터페이스와-제네릭">함수형 인터페이스와 제네릭</h2>
<ul>
<li>제네릭을 사용해야 하는 이유 : 
  제네릭을 도입하면 코드 재사용성과 타입 안정성을 동시에 확보할 수 있다.
  제네릭을 사용하면 다양한 데이터 타입을 처리할 수 있으면서, 컴파일 시점에 타입을 체크하여 런타임 오류를 방지할 수 있다.</li>
<li>예시 : 문자 타입과 숫자 타입을 처리하는 두 개의 함수형 인터페이스를 사용하여 각각 다른 타입을 처리할 수 있도록 하고, 제네릭을 도입하여 코드의 중복을 줄인다.</li>
</ul>
<h2 id="람다와-타겟-타입">람다와 타겟 타입</h2>
<ul>
<li>람다 표현식 :
  람다 표현식은 자체적으로 타입을 가지고 있지 않으며, 타겟 타입에 따라 타입이 결정된다.
  예를 들어 Function&lt;Integer, String&gt; 에 람다식을 대입하면 그 타입은 Function&lt;Integer, String&gt;으로 결정된다.</li>
<li>타겟 타입 : 
  람다는 대입되는 함수형 인터페이스의 타입에 맞춰 타입이 정해지므로, 같은 람다라도 서로 다른 함수형 인터페이스에 대입될 수 있다.</li>
</ul>
<h2 id="자바가-제공하는-기본-함수형-인터페이스">자바가 제공하는 기본 함수형 인터페이스</h2>
<h3 id="functiont-r">Function&lt;T, R&gt;</h3>
<ul>
<li>하나의 입력 T를 받아 결과 R을 반환하는 함수형 인터페이스로, 데이터를 변환하는 데 사용된다.<h3 id="consumer-t">Consumer&lt; T&gt;</h3>
</li>
<li>하나의 입력 T를 받아 결과 없이 처리하는 함수형 인터페이스로, 주로 로그 출력, 데이터 저장 등의 작업에서 사용된다.<h3 id="supplier-t">Supplier&lt; T&gt;</h3>
</li>
<li>매개변수 없이 결과만 반환하는 함수형 인터페이스로, 값을 생성하거나 지연 초기화할 때 사용된다.<h3 id="runnable">Runnable</h3>
</li>
<li>매개변수와 반환값이 모두 없는 함수형 인터페이스로, 주로 멀티스레딩 작업에서 사용된다.</li>
</ul>
<h2 id="특화-함수형-인터페이스">특화 함수형 인터페이스</h2>
<h3 id="predivate-t">Predivate&lt; T&gt;</h3>
<ul>
<li>주어진 입력을 받아 boolean을 반환하여 조건 검사나 필터링에 사용된다.</li>
<li>예를 들어, 짝수 여부를 판단할 때 사용된다.<h3 id="unaryoperator-t">UnaryOperator&lt; T&gt;</h3>
</li>
<li>입력과 반환 타입이 동일한 단항 연산을 수행하는 함수형 인터페이스로, 숫자 제곱이나 문자열 대문자 변환 등에 사용된다.<h3 id="binaryoperator-t">BinaryOperator&lt; T&gt;</h3>
</li>
<li>두 개의 입력을 받아 같은 타입의 출력을 반환하는 함수형 인터페이스로, 두 수의 덧셈이나 곱셈과 같은 연산에 사용된다.</li>
</ul>
<h2 id="입력값이-여러-개인-경우">입력값이 여러 개인 경우</h2>
<h3 id="bifunction">BiFunction</h3>
<ul>
<li>두 개의 입력을 받아 하나의 결과를 반환하는 함수형 인터페이스이다.<h3 id="trifunction">TriFunction</h3>
</li>
<li>기본적으로 제공되지 않지만, 세 개 이상의 입력을 처리할 필요가 있을 때 커스텀 인터페이스로 정의하여 사용할 수 있다.</li>
</ul>
<h2 id="기본형-지원-함수형-인터페이스">기본형 지원 함수형 인터페이스</h2>
<ul>
<li><p>자바의 제네릭은 기본형 타입을 직접 다룰 수 없기 때문에, IntFunction, ToIntFunction, IntUnaryOperator와 같은 기본형 전용 함수형 인터페이스를 제공한다.</p>
</li>
<li><p>예시:
IntFunction은 매개변수로 int 타입을 받고, ToIntFunction은 반환값으로 int 타입을 반환한다.</p>
</li>
</ul>
<h2 id="자주-사용되는-함수형-인터페이스의-예시">자주 사용되는 함수형 인터페이스의 예시</h2>
<ul>
<li><p>filter:
조건에 맞는 요소만 뽑아내는 함수로, <strong>Predicate</strong>를 사용하여 조건을 검사한다.</p>
</li>
<li><p>map:
리스트의 각 요소를 변환하는 함수로, <strong>UnaryOperator</strong>나 <strong>Function</strong>을 사용하여 변환 작업을 수행한다.</p>
</li>
<li><p>reduce:
리스트 요소를 하나로 축약하는 함수로, <strong>BinaryOperator</strong>를 사용하여 요소를 결합하거나 연산을 수행한다.</p>
</li>
</ul>
<h2 id="결론">결론</h2>
<ul>
<li>함수형 인터페이스와 람다를 사용하면 코드가 더 간결해지고 유연해진다.</li>
<li>제네릭을 활용하면 코드 재사용성을 높이고 타입 안정성을 보장할 수 있다.</li>
<li>자바에서 제공하는 다양한 기본 함수형 인터페이스를 사용하면, 불필요한 인터페이스를 만들지 않아도 되고, 코드의 유지보수성도 향상된다.</li>
<li>각 함수형 인터페이스는 그 사용 의도를 명확히 나타내기 때문에, 코드를 이해하기 쉽고 유지보수가 용이해진다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[람다]]></title>
            <link>https://velog.io/@daun_jung/%EB%9E%8C%EB%8B%A4</link>
            <guid>https://velog.io/@daun_jung/%EB%9E%8C%EB%8B%A4</guid>
            <pubDate>Sat, 12 Apr 2025 15:50:08 GMT</pubDate>
            <description><![CDATA[<h1 id="람다-정의">람다 정의</h1>
<ul>
<li>람다는 익명 함수이다. 따라서 이름 없이 함수를 표현한다.</li>
</ul>
<pre><code class="language-java">// 일반 함수 - 이름이 있음
public int add(int x) {
     return x + 1;
}

// 람다 - 이름이 없음
(int x) -&gt; {return x + 1;}</code></pre>
<ul>
<li>람다는 표현이 간결하다.</li>
<li>람다는 변수처럼 다룰 수 있다.</li>
<li>람다도 클래스가 만들어지고, 인스턴스가 생성된다.</li>
<li>익명 클래스의 경우 $ 로 구분하고 뒤에 숫자가 붙는다.</li>
<li>람다의 경우 $$ 로 구분하고 뒤에 복잡한 문자가 붙는다.</li>
<li>람다를 사용하면 익명 클래스 사용의 보일러플레이트 코드를 크게 줄이고, 간결한 코드로 생산성과 가독성을 높일
수 있다.</li>
<li>대부분의 익명 클래스는 람다로 대체할 수 있다.</li>
<li>람다를 사용할 때 new 키워드를 사용하지 않지만, 람다도 익명 클래스처럼 인스턴스가 생성된다.</li>
</ul>
<h1 id="함수형-인터페이스">함수형 인터페이스</h1>
<ul>
<li>함수형 인터페이스는 정확히 하나의 추상 메서드를 가지는 인터페이스를 말한다.</li>
<li>람다는 추상 메서드가 하나인 함수형 인터페이스에만 할당할 수 있다.</li>
<li>단일 추상 메서드를 줄여서 SAM(Single Abstract Method)이라 한다.</li>
<li>참고로 람다는 클래스, 추상 클래스에는 할당할 수 없다. 오직 단일 추상 메서드를 가지는 인터페이스에만 할당할 수 있다.</li>
</ul>
<p>여러 추상 메서드</p>
<pre><code class="language-java">package lambda.lambda1;
public interface NotSamInterface {
     void run();
     void go();
}</code></pre>
<ul>
<li>인터페이스의 메서드 앞에는 abstract (추상)이 생략되어 있다. (자바 기본!)</li>
<li>여기에는 run() , go() 두 개의 추상 메서드가 선언되어 있다.</li>
<li>단일 추상 메서드(SAM)가 아니다. 이 인터페이스에는 람다를 할당할 수 없다.</li>
</ul>
<p>단일 추상 메서드</p>
<pre><code class="language-java">package lambda.lambda1;
public interface SamInterface {
     void run();
}</code></pre>
<ul>
<li>여기에는 run() 한 개의 추상 메서드만 선언되어 있다.</li>
<li>단일 추상 메서드(SAM)이다. 이 인터페이스에는 람다를 할당할 수 있다.</li>
</ul>
<pre><code class="language-java">package lambda.lambda1;
public class SamMain {
     public static void main(String[] args) {
         SamInterface samInterface = () -&gt; {
             System.out.println(&quot;sam&quot;);
     };
     samInterface.run();

 // 컴파일 오류
 /*
 NotSamInterface notSamInterface = () -&gt; {
 System.out.println(&quot;not sam&quot;);
 };
 notSamInterface.run(); // ?
 notSamInterface.go(); // ?
 */
 }
}</code></pre>
<p>=&gt; </p>
<ul>
<li>인터페이스는 여러 메서드(함수)를 선언할 수 있다. 여기서는 run() , go() 두 메서드가 존재한다.</li>
<li>이 함수를 NotSamInterface 에 있는 run() 또는 go() 둘 중에 하나에 할당해야 하는 문제가 발생한다.</li>
<li>자바는 이러한 문제를 해결하기 위해, 단 하나의 추상 메서드(SAM: Single Abstract Method)만을 포함하는 함수형
인터페이스에만 람다를 할당할 수 있도록 제한했다.</li>
</ul>
<h2 id="functionalinterface">@FunctionalInterface</h2>
<pre><code class="language-java">public class Car {
     public void move() {
         System.out.println(&quot;차를 이동합니다.&quot;);
     }
}
public class ElectricCar extends Car {
     @Override
     public void movee() {
         System.out.println(&quot;전기차를 빠르게 이동합니다.&quot;);
     }
}</code></pre>
<p>메서드를 재정의할 때 실수로 재정의할 메서드 이름을 다르게 적으면 재정의가 되지 않는다. 이 예제에서 부모는
move() 인데 자식은 movee() 라고 e 를 하나 더 잘못 적었다. 이런 문제를 컴파일 단계에서 원천적으로 막기 위해
@Override 애노테이션을 사용한다. 이 애노테이션 덕분에 개발자가 할 수 있는 실수를 컴파일 단계에서 막을 수 있
고, 또 개발자는 이 메서드가 재정의 메서드인지 명확하게 인지할 수 있다.</p>
<h1 id="람다와-시그니처">람다와 시그니처</h1>
<p>람다를 함수형 인터페이스에 할당할 때는 메서드의 형태를 정의하는 요소인 메서드 시그니처가 일치해야 한다.
메서드 시그니처의 주요 구성 요소는 다음과 같다.</p>
<ol>
<li>메서드 이름</li>
<li>매개변수의 수와 타입(순서 포함)</li>
<li>반환 타입</li>
</ol>
<p>MyFunction 예시
예를 들어 MyFunction 의 apply 메서드를 살펴보자.</p>
<pre><code class="language-java">@FunctionalInterface
public interface MyFunction {
     int apply(int a, int b);
}</code></pre>
<p>이 메서드의 시그니처</p>
<ul>
<li><p>이름: apply</p>
</li>
<li><p>매개변수: int , int</p>
</li>
<li><p>반환 타입: int</p>
<pre><code class="language-java">MyFunction myFunction = (int a, int b) -&gt; {
   return a + b;
};</code></pre>
</li>
<li><p>람다는 익명 함수이므로 시그니처에서 이름은 제외하고, 매개변수, 반환 타입이 함수형 인터페이스에 선언한 메서드와 맞아야 한다.</p>
</li>
<li><p>이 람다는 매개변수로 int a , int b , 그리고 반환 값으로 a + b 인 int 타입을 반환하므로 시그니처가 맞다. 따라서 람다를 함수형 인터페이스에 할당할 수 있다.</p>
</li>
<li><p>참고로 람다의 매개변수 이름은 함수형 인터페이스에 있는 메서드 매개변수의 이름과 상관없이 자유롭게 작성해도 된다. 타입과 순서만 맞으면 된다.</p>
</li>
<li><p>매개변수 타입: 생략 가능하지만 필요하다면 명시적으로 작성할 수 있다.</p>
</li>
<li><p>반환 타입: 문법적으로 명시할 수 없고, 식의 결과를 보고 컴파일러가 항상 추론한다.</p>
</li>
<li><p>람다는 보통 간략하게 사용하는 것을 권장한다.</p>
<ul>
<li>단일 표현식이면 중괄호와 리턴을 생략하자.</li>
<li>타입 추론을 통해 매개변수의 타입을 생략하자. (컴파일러가 추론할 수 있다면, 생략하자)</li>
</ul>
</li>
</ul>
<h1 id="람다의-전달">람다의 전달</h1>
<p>람다를 변수에 대입하기</p>
<pre><code class="language-java">package lambda.lambda2;

import lambda.MyFunction;

// 1. 람다를 변수에 대입하기
public class LambdaPassMain1 {

 public static void main(String[] args) {
     MyFunction add = (a, b) -&gt; a + b;
     MyFunction sub = (a, b) -&gt; a - b;

     System.out.println(&quot;add.apply(1, 2) = &quot; + add.apply(1, 2));
     System.out.println(&quot;sub.apply(1, 2) = &quot; + sub.apply(1, 2));

     MyFunction cal = add;
     System.out.println(&quot;cal(add).apply(1, 2) = &quot; + cal.apply(1, 2));

     cal = sub;
     System.out.println(&quot;cal(sub).apply(1, 2) = &quot; + cal.apply(1, 2));
     }
}</code></pre>
<p>람다를 메서드(함수)에 전달하기
앞서 본 것과 같이 람다는 변수에 전달할 수 있다.
같은 원리로 람다를 매개변수를 통해 메서드(함수)에 전달할 수 있다.</p>
<pre><code class="language-java">package lambda.lambda2;
import lambda.MyFunction;

// 2. 람다를 메서드(함수)에 전달하기
public class LambdaPassMain2 {

 public static void main(String[] args) {
     MyFunction add = (a, b) -&gt; a + b;
     MyFunction sub = (a, b) -&gt; a - b;

     System.out.println(&quot;변수를 통해 전달&quot;);
     calculate(add);
     calculate(sub);

     System.out.println(&quot;람다를 직접 전달&quot;);
     calculate((a, b) -&gt; a + b);
     calculate((a, b) -&gt; a - b);
 }

 static void calculate(MyFunction function) {
     int a = 1;
     int b = 2;

     System.out.println(&quot;계산 시작&quot;);
     int result = function.apply(a, b);
     System.out.println(&quot;계산 결과: &quot; + result);
 }
}</code></pre>
<p>람다를 반환하기</p>
<pre><code class="language-java">package lambda.lambda2;
import lambda.MyFunction;

// 3. 람다를 반환하기
public class LambdaPassMain3 {
     public static void main(String[] args) {
         MyFunction add = getOperation(&quot;add&quot;);
       System.out.println(&quot;add.apply(1, 2) = &quot; + add.apply(1, 2));
       MyFunction sub = getOperation(&quot;sub&quot;);
       System.out.println(&quot;sub.apply(1, 2) = &quot; + sub.apply(1, 2));
       MyFunction xxx = getOperation(&quot;xxx&quot;);
       System.out.println(&quot;xxx.apply(1, 2) = &quot; + xxx.apply(1, 2));
 }

 // 람다를 반환하는 메서드
 static MyFunction getOperation(String operator) {
   switch (operator) {
     case &quot;add&quot;:
         return (a, b) -&gt; a + b;
     case &quot;sub&quot;:
         return (a, b) -&gt; a - b;
     default:
         return (a, b) -&gt; 0;
     }
   }
}</code></pre>
<h1 id="고차-함수">고차 함수</h1>
<p>고차 함수(Higher-Order Function)</p>
<ul>
<li>고차 함수는 함수를 값처럼 다루는 함수를 뜻한다.</li>
<li>일반적으로 다음 두 가지 중 하나를 만족하면 고차 함수라 한다.<ul>
<li>함수를 인자로 받는 함수(메서드)</li>
<li>함수를 반환하는 함수(메서드)</li>
</ul>
</li>
</ul>
<p>함수를 인자로 받는 경우</p>
<pre><code class="language-java">// 함수(람다)를 매개변수로 받음
static void calculate(MyFunction function) {
 // ...
}</code></pre>
<p>함수를 반환하는 경우</p>
<pre><code class="language-java">// 함수(람다)를 반환
static MyFunction getOperation(String operator) {
 // ...
 return (a, b) -&gt; a + b;
}</code></pre>
<ul>
<li>즉, 매개변수나 반환값에 함수(또는 람다)를 활용하는 함수가 고차 함수에 해당한다.</li>
<li>자바에서 람다(익명 함수)는 함수형 인터페이스를 통해서만 전달할 수 있다.</li>
<li>자바에서 함수를 주고받는다는 것은 &quot;함수형 인터페이스를 구현한 어떤 객체(람다든 익명 클래스든)를 주고받는 것&quot;과 동의어이다. (함수형 인터페이스는 인터페이스이므로 익명 클래스, 람다 둘다 대입할 수 있다. 하지만 실질적으로 함수형 인터페이스에는 람다를 주로 사용한다.)</li>
</ul>
<h2 id="정리">&lt;정리&gt;</h2>
<ol>
<li>람다란?<ul>
<li>자바 8에서 도입된 익명 함수로, 이름 없이 간결하게 함수를 표현한다.</li>
<li>예: (x) -&gt; x + 1</li>
<li>익명 클래스보다 보일러플레이트 코드를 줄여 생산성과 가독성을 높이는 문법 설탕 역할.</li>
</ul>
</li>
<li>함수형 인터페이스<ul>
<li>람다를 사용할 수 있는 기반으로, 단일 추상 메서드(SAM)만 포함하는 인터페이스.</li>
<li>예: @FunctionalInterface 로 보장하며, 하나의 메서드만 정의.</li>
<li>여러 메서드가 있으면 람다 할당 불가(모호성 방지).</li>
</ul>
</li>
<li>람다 문법<ul>
<li>기본 형태: (매개변수) -&gt; {본문}</li>
<li>생략 가능<ul>
<li>단일 표현식(본문, 반환 생략): x -&gt; x + 1</li>
<li>타입 추론: (int x) -&gt; x (x) -&gt; x</li>
<li>매개변수 괄호(단일 매개변수일 때): x -&gt; x</li>
</ul>
</li>
<li>시그니처(매개변수 수/타입, 반환 타입)이 함수형 인터페이스와 일치해야 함.</li>
</ul>
</li>
<li>람다 활용<ul>
<li>변수 대입: MyFunction f = (a, b) -&gt; a + b; 처럼 람다 인스턴스를 변수에 저장.</li>
<li>메서드 전달: calculate((a, b) -&gt; a + b) 로 함수처럼 전달 가능.</li>
<li>반환: return (a, b) -&gt; a + b; 로 메서드에서 람다를 반환.</li>
</ul>
</li>
<li>고차 함수<ul>
<li>함수를 인자나 반환값으로 다루는 함수(예: filter , map , reduce ).</li>
<li>자바에서는 함수형 인터페이스와 람다로 구현하며, 코드의 유연성과 추상화 수준을 높임.</li>
<li>예: List<Integer> filter(List<Integer> list, MyPredicate p) 는 조건 함수를 받아 동작.</li>
</ul>
</li>
<li>기타<ul>
<li>람다는 익명 클래스를 간소화한 도구지만, 내부적으로 인스턴스가 생성됨.</li>
<li>반복 연습으로 문법과 활용법을 익히는 것이 중요!</li>
</ul>
</li>
</ol>
<p>[문제와 풀이 2, 3이 매우 중요!]</p>
<h1 id="문제와-풀이-1">문제와 풀이 1</h1>
<h3 id="문제-1-중복되는-메시지-출력-로직-리팩토링">문제 1. 중복되는 메시지 출력 로직 리팩토링</h3>
<p>문제 설명
다음 코드는 화면에 여러 종류의 인삿말 메시지를 출력하지만, 모든 메서드마다 === 시작 === 과 === 끝 === 을 출력하는 로직이 중복되어 있다. 중복되는 코드를 제거하고, 변하는 부분(인삿말 메시지)만 매개변수로 받도록 리팩토링 해라</p>
<p>예시 코드</p>
<pre><code class="language-java">package lambda.ex1;

public class M1Before {
 public static void greetMorning() {
   System.out.println(&quot;=== 시작 ===&quot;);
   System.out.println(&quot;Good Morning!&quot;);
   System.out.println(&quot;=== 끝 ===&quot;);
 }
 public static void greetAfternoon() {
   System.out.println(&quot;=== 시작 ===&quot;);
   System.out.println(&quot;Good Afternoon!&quot;);
   System.out.println(&quot;=== 끝 ===&quot;);
 }
 public static void greetEvening() {
   System.out.println(&quot;=== 시작 ===&quot;);
   System.out.println(&quot;Good Evening!&quot;);
   System.out.println(&quot;=== 끝 ===&quot;);
 }
 public static void main(String[] args) {
   greetMorning();
   greetAfternoon();
   greetEvening();
 }
}</code></pre>
<p>정답</p>
<pre><code class="language-java">package lambda.ex1;
public class M1After {
 // 하나의 메서드로 합치고, 매개변수(문자열)만 다르게 받아 처리
 public static void greet(String message) {
   System.out.println(&quot;=== 시작 ===&quot;);
   System.out.println(message);
   System.out.println(&quot;=== 끝 ===&quot;);
 }
 public static void main(String[] args) {
   greet(&quot;Good Morning!&quot;);
   greet(&quot;Good Afternoon!&quot;);
   greet(&quot;Good Evening!&quot;);
 }
}</code></pre>
<h3 id="문제-2-값-매개변수화---다양한-단위를-매개변수로-받기">문제 2. 값 매개변수화 - 다양한 단위를 매개변수로 받기</h3>
<p>문제 설명
다음 코드는, 주어진 숫자(예: 10)를 특정 단위(예: &quot;kg&quot;)로 출력하는 간단한 메서드를 작성한 예시이다.
숫자와 단위를 나누고 재사용 가능한 메서드를 사용하도록 코드를 수정해라.</p>
<p>예시 코드</p>
<pre><code class="language-java">public class M2Before {
 public static void print1() {
     System.out.println(&quot;무게: 10kg&quot;);
 }
 public static void print2() {
     System.out.println(&quot;무게: 50kg&quot;);
 }
 public static void print3() {
     System.out.println(&quot;무게: 200g&quot;);
 }
 public static void print4() {
     System.out.println(&quot;무게: 40g&quot;);
 }
 public static void main(String[] args) {
   print1();
   print2();
   print3();
   print4();
 }
}</code></pre>
<p>정답</p>
<pre><code class="language-java">package lambda.ex1;
public class M2After {
   // 숫자(무게)와 단위 모두 매개변수화
   public static void print(int weight, String unit) {
   System.out.println(&quot;무게: &quot; + weight + unit);
 }
 public static void main(String[] args) {
   print(10, &quot;kg&quot;);
   print(50, &quot;kg&quot;);
   print(200, &quot;g&quot;);
   print(40, &quot;g&quot;);
 }
}</code></pre>
<h3 id="문제-3-동작-매개변수화---익명-클래스로-다른-로직-전달">문제 3. 동작 매개변수화 - 익명 클래스로 다른 로직 전달</h3>
<p>문제 설명
1부터 N까지 더하는 로직과, 배열을 정렬하는( Arrays.sort() ) 로직을 각각 실행하고, 이 두 가지 로직 모두 &quot;실행
에 걸린 시간을 측정&quot;하고 싶다.
&quot;실행 시간 측정&quot; 로직은 변하지 않는 부분
&quot;실행할 로직&quot;은 바뀌는 부분(1부터 N 합 구하기 vs 배열 정렬)
이 문제는 람다를 사용하지 말고 익명 클래스를 사용해서 풀어라
문제</p>
<ol>
<li>앞서 정의한 Procedure (추상 메서드 run() ) 함수형 인터페이스를 사용해라.</li>
<li>measure(Procedure p) 메서드 안에서
실행 전 시간 기록
p.run() 실행
실행 후 시간 기록
걸린 시간 출력</li>
<li>main() 에서 익명 클래스 두 가지를 만들어 각각 실행 시간을 측정해라.
(1) 1부터 N까지 합을 구하는 로직 ( measure 메서드 호출)
(2) 배열을 정렬하는 로직 ( measure 메서드 호출)
measure 메서드는 총 2번 호출된다.
(1) 1부터 N까지 합을 구하는 로직 ( measure 메서드 호출)<pre><code class="language-java">int N = 100;
long sum = 0;
for (int i = 1; i &lt;= N; i++) {
  sum += i;
}</code></pre>
(2) 배열을 정렬하는 로직 ( measure 메서드 호출)<pre><code class="language-java">int[] arr = { 4, 3, 2, 1 };
System.out.println(&quot;원본 배열: &quot; + Arrays.toString(arr));
Arrays.sort(arr);
System.out.println(&quot;배열 정렬: &quot; + Arrays.toString(arr));</code></pre>
</li>
</ol>
<p>정답
예시 함수형 인터페이스</p>
<pre><code class="language-java">package lambda;
@FunctionalInterface
    public interface Procedure {
         void run();
}</code></pre>
<pre><code class="language-java">package lambda.ex1;
import lambda.Procedure;
import java.util.Arrays;

public class M3MeasureTime {
 // 공통: 실행 시간 측정 메서드
 public static void measure(Procedure p) {
   long startNs = System.nanoTime();
   p.run(); // 바뀌는 로직 실행 (익명 클래스 or 람다로 전달)
   long endNs = System.nanoTime();
   System.out.println(&quot;실행 시간: &quot; + (endNs - startNs) + &quot;ns&quot;);
 }
 public static void main(String[] args) {
 // 1. 익명 클래스로 1부터 N까지 합 구하기
   measure(new Procedure() {
   @Override
   public void run() {
     int N = 100;
     long sum = 0;
     for (int i = 1; i &lt;= N; i++) {
     sum += i;
 }
 System.out.println(&quot;[1부터 &quot; + N + &quot;까지 합] 결과: &quot; + sum);
 }
 });
 // 2. 익명 클래스로 배열 정렬
 measure(new Procedure() {
   @Override
   public void run() {
     int[] arr = { 4, 3, 2, 1 };
     System.out.println(&quot;원본 배열: &quot; + Arrays.toString(arr));
     Arrays.sort(arr);
     System.out.println(&quot;배열 정렬: &quot; + Arrays.toString(arr));
     }
     });
 }
}</code></pre>
<h3 id="문제-4-람다로-변경---간결하게-코드-작성하기">문제 4. 람다로 변경 - 간결하게 코드 작성하기</h3>
<p>문제 설명
이전 문제에서 익명 클래스로 작성한 부분을 람다로 변경해라.
measure() 메서드와 Procedure 인터페이스는 그대로 둔다.
main() 에서 익명 클래스를 사용하지 말고, 람다를 이용하여 더욱 간결하게 코드를 작성해라.
정답</p>
<pre><code class="language-java">package lambda.ex1;
import lambda.Procedure;
import java.util.Arrays;

public class M4MeasureTime {
 // 공통: 실행 시간 측정 메서드
   public static void measure(Procedure p) {
     long startNs = System.nanoTime();
     p.run(); // 바뀌는 로직 실행 (익명 클래스 or 람다로 전달)
     long endNs = System.nanoTime();
     System.out.println(&quot;실행 시간: &quot; + (endNs - startNs) + &quot;ns\n&quot;);
   }
 public static void main(String[] args) {
   // 1. 람다로 1부터 N까지 합 구하기
   measure(() -&gt; {
   int N = 100;
   long sum = 0;
   for (int i = 1; i &lt;= N; i++) {
       sum += i;
   }
   System.out.println(&quot;[1부터 &quot; + N + &quot;까지 합] 결과: &quot; + sum);
   });
   // 2. 람다로 배열 정렬
   measure(() -&gt; {
     int[] arr = { 4, 3, 2, 1 };
     System.out.println(&quot;원본 배열: &quot; + Arrays.toString(arr));
     Arrays.sort(arr);
     System.out.println(&quot;[배열 정렬] 결과: &quot; + Arrays.toString(arr));
     });
     }
}</code></pre>
<h3 id="문제-5-고차-함수high-order-function---함수를-반환하기">문제 5. 고차 함수(High-Order Function) - 함수를 반환하기</h3>
<p>문제 설명
&quot;함수를 반환&quot;하는 방식도 연습해보자. 두 정수를 받아서 연산하는 MyFunction 인터페이스를 사용해보자.</p>
<pre><code class="language-java">package lambda;
@FunctionalInterface
public interface MyFunction {
     int apply(int a, int b);
    }</code></pre>
<p>static MyFunction getOperation(String operator) 라는 정적 메서드를 만들어라.
매개변수인 operator 에 따라 다음과 같은 내용을 전달하고 반환해라.
operator 가 &quot;add&quot;면, (a, b) 를 받아 a + b 를 리턴하는 람다를 반환해라.
&quot;sub&quot;면, a - b 를 리턴하는 람다를 반환해라.
그 외의 경우는 항상 0을 리턴하는 람다를 반환해라.
main() 메서드에서 getOperation(&quot;add&quot;) , getOperation(&quot;sub&quot;) , getOperation(&quot;xxx&quot;) 를
각각 호출해서 반환된 람다를 실행해라.
예시 출력</p>
<pre><code class="language-java">add(1, 2) = 3
sub(1, 2) = -1
xxx(1, 2) = 0 // 그 외의 경우</code></pre>
<p>정답</p>
<pre><code class="language-java">package lambda.ex1;
import lambda.MyFunction;

public class M5Return {
     // operator에 따라 다른 람다(=함수)를 반환
     public static void main(String[] args) {
       MyFunction add = getOperation(&quot;add&quot;);
       System.out.println(&quot;add(1, 2) = &quot; + add.apply(1, 2));

       MyFunction sub = getOperation(&quot;sub&quot;);
       System.out.println(&quot;sub(1, 2) = &quot; + sub.apply(1, 2));

       MyFunction xxx = getOperation(&quot;xxx&quot;);
       System.out.println(&quot;xxx(1, 2) = &quot; + xxx.apply(1, 2));
 }
 public static MyFunction getOperation(String operator) {
   switch (operator) {
     case &quot;add&quot;:
     return (a, b) -&gt; a + b;
     case &quot;sub&quot;:
     return (a, b) -&gt; a - b;
     default:
     return (a, b) -&gt; 0; // 잘못된 연산자일 경우 0 반환
     }
   }
}</code></pre>
<h2 id="문제와-풀이2">문제와 풀이2</h2>
<p>이번 문제들은 이후에 설명할 스트림은 물론이고, 함수형 프로그래밍의 개념을 이해하기 위해 반드시 반복해서 풀어보고, 또 이해해야 한다.
각 문제에서 요구하는핵심 사항은 &quot;함수를 매개변수로 받거나, 함수를 반환&quot; 하는 구조를 구현하는 것이다. 
람다가 아직 익숙하지 않을 것이니 먼저 익명 클래스로 구현해보고 그 다음에 람다로 구현해보자.</p>
<p>참고: 고차 함수(Higher-Order Function)란?
함수를 인자로 받거나, 함수를 반환하는 함수
자바에서는 함수형 인터페이스에 익명 클래스나 람다를 담아서 주고받음으로써 고차 함수를 구현할 수 있다.</p>
<h3 id="문제-1-filter-함수-구현하기">문제 1. filter 함수 구현하기</h3>
<p>요구 사항</p>
<ol>
<li>정수 리스트가 주어졌을 때, 특정 조건에 맞는 요소들만 뽑아내는 filter 함수를 직접 만들어보자.</li>
<li>filter(List<Integer> list, MyPredicate predicate) 형식의 정적 메서드를 하나 작성한다.
MyPredicate 는 함수형 인터페이스이며, boolean test(int value); 같은 메서드를 가진다.</li>
<li>main() 에서 예시로 다음과 같은 상황을 실습해보자.
리스트: [-3, -2, -1, 1, 2, 3, 5]
조건 1: 음수(negative)만 골라내기
조건 2: 짝수(even)만 골라내기
예시 실행
원본 리스트: [-3, -2, -1, 1, 2, 3, 5]
음수만: [-3, -2, -1]
짝수만: [-2, 2]</li>
</ol>
<p>함수형 인터페이스 예시</p>
<pre><code class="language-java">package lambda.ex2;
@FunctionalInterface
public interface MyPredicate {
     boolean test(int value);
}</code></pre>
<p>기본 코드 예시</p>
<pre><code class="language-java">package lambda.ex2;
import java.util.ArrayList;
import java.util.List;
public class FilterExample {
 // 고차 함수, 함수를 인자로 받아서 조건에 맞는 요소만 뽑아내는 filter
 public static List&lt;Integer&gt; filter(List&lt;Integer&gt; list, MyPredicate
predicate) {
   List&lt;Integer&gt; result = new ArrayList&lt;&gt;();
   for (int val : list) {
           if (predicate.test(val)) {
           result.add(val);
           }
   }
   return result;
 }
 public static void main(String[] args) {
     List&lt;Integer&gt; numbers = List.of(-3, -2, -1, 1, 2, 3, 5);
     System.out.println(&quot;원본 리스트: &quot; + numbers);
     // 1. 음수(negative)만 뽑아내기
     // 코드 작성
     // 2. 짝수(even)만 뽑아내기
     // 코드 작성
     }
}</code></pre>
<p>정답 - 익명 클래스</p>
<pre><code class="language-java">package lambda.ex2;
import java.util.ArrayList;
import java.util.List;

public class FilterExampleEx1 {
 // 고차 함수, 함수를 인자로 받아서 조건에 맞는 요소만 뽑아내는 filter
 public static List&lt;Integer&gt; filter(List&lt;Integer&gt; list, MyPredicate
predicate) {
   List&lt;Integer&gt; result = new ArrayList&lt;&gt;();
   for (int val : list) {
     if (predicate.test(val)) {
     result.add(val);
     }
   }
   return result;
   }
 public static void main(String[] args) {
     List&lt;Integer&gt; numbers = List.of(-3, -2, -1, 1, 2, 3, 5);
     System.out.println(&quot;원본 리스트: &quot; + numbers);
     // 1. 음수(negative)만 뽑아내기
     List&lt;Integer&gt; negatives = filter(numbers, new MyPredicate() {
     @Override
     public boolean test(int value) {
         return value &lt; 0;
     }
 });
   System.out.println(&quot;음수만: &quot; + negatives);
   // 2. 짝수(even)만 뽑아내기
   List&lt;Integer&gt; evens = filter(numbers, new MyPredicate() {
     @Override
     public boolean test(int value) {
         return value % 2 == 0;
         }
     });
     System.out.println(&quot;짝수만: &quot; + evens);
   }
}</code></pre>
<p>정답 - 람다</p>
<pre><code class="language-java">package lambda.ex2;
import java.util.ArrayList;
import java.util.List;
public class FilterExampleEx2 {
   // 고차 함수, 함수를 인자로 받아서 조건에 맞는 요소만 뽑아내는 filter
   public static List&lt;Integer&gt; filter(List&lt;Integer&gt; list, MyPredicate
  predicate) {
     List&lt;Integer&gt; result = new ArrayList&lt;&gt;();
     for (int val : list) {
         if (predicate.test(val)) {
         result.add(val);
     }
     }
     return result;
 }
 public static void main(String[] args) {
   List&lt;Integer&gt; numbers = List.of(-3, -2, -1, 1, 2, 3, 5);
   System.out.println(&quot;원본 리스트: &quot; + numbers);
   // 1. 음수(negative)만 뽑아내기
   List&lt;Integer&gt; negatives = filter(numbers, value -&gt; value &lt; 0);
   System.out.println(&quot;음수만: &quot; + negatives);
   // 2. 짝수(even)만 뽑아내기
   List&lt;Integer&gt; evens = filter(numbers, value -&gt; value % 2 == 0);
   System.out.println(&quot;짝수만: &quot; + evens);
   }
}</code></pre>
<ul>
<li>filter() 메서드가 MyPredicate 라는 &quot;조건 함수&quot;를 받아서, test() 가 true 일 때만 결과 리스트에 추
가한다.</li>
<li>이처럼 함수를 인자로 받아서 로직을 결정하는 형태가 전형적인 고차 함수이다.
문제 2. map 함수 구현하기</li>
</ul>
<p>요구 사항</p>
<ol>
<li>문자열 리스트를 입력받아, 각 문자열을 어떤 방식으로 변환(map, mapping)할지 결정하는 함수( map )를 만들
어보자</li>
<li>map(List<String> list, StringFunction func) 형태로 구현한다.
StringFunction 은 함수형 인터페이스이며, String apply(String s); 같은 메서드를 가진다.</li>
<li>main() 에서 다음 변환 로직들을 테스트해보자.
변환 1: 모든 문자열을 대문자로 변경
변환 2: 문자열 앞 뒤에 <strong>* 를 붙여서 반환(예: &quot;hello&quot; → &quot;*</strong>hello***&quot; )
예시 실행<pre><code>원본 리스트: [hello, java, lambda]
대문자 변환 결과: [HELLO, JAVA, LAMBDA]
특수문자 데코 결과: [***hello***, ***java***, ***lambda***]</code></pre>함수형 인터페이스<pre><code class="language-java">package lambda.ex2;
@FunctionalInterface
public interface StringFunction {
  String apply(String s);
}</code></pre>
코드 예시<pre><code class="language-java">package lambda.ex2;
import java.util.List;
public class MapExample {
// 고차 함수, 함수를 인자로 받아, 리스트의 각 요소를 변환
public static List&lt;String&gt; map(List&lt;String&gt; list, StringFunction func) {
  // 코드 작성
  return null; // 제거하고 적절한 객체를 반환
}
public static void main(String[] args) {
  List&lt;String&gt; words = List.of(&quot;hello&quot;, &quot;java&quot;, &quot;lambda&quot;);
  System.out.println(&quot;원본 리스트: &quot; + words);
  // 1. 대문자 변환
  // 코드 작성
  // 2. 앞뒤에 *** 붙이기 (람다로 작성)
  // 코드 작성
}
}</code></pre>
정답<pre><code class="language-java">package lambda.ex2;
</code></pre>
</li>
</ol>
<p>import java.util.ArrayList;
import java.util.List;</p>
<p>public class MapExample {
    // 고차 함수, 함수를 인자로 받아, 리스트의 각 요소를 변환
    public static List<String> map(List<String> list, StringFunction func) {
        List<String> result = new ArrayList&lt;&gt;();
        for (String str : list) {
            result.add(func.apply(str));
        }
        return result;
    }</p>
<pre><code>public static void main(String[] args) {
    List&lt;String&gt; words = List.of(&quot;hello&quot;, &quot;java&quot;, &quot;lambda&quot;);
    System.out.println(&quot;원본 리스트: &quot; + words);

    // 1. 대문자 변환
    List&lt;String&gt; upperList = map(words, s -&gt; s.toUpperCase());
    System.out.println(&quot;대문자 변환 결과: &quot; + upperList);

    // 2. 앞뒤에 *** 붙이기 (람다로 작성)
    List&lt;String&gt; decoratedList = map(words, s -&gt; &quot;***&quot; +***</code></pre><pre><code>### 문제와 풀이3
문제 3. reduce(또는 fold) 함수 구현하기
요구 사항
1. 정수 리스트를 받아서, 모든 값을 하나로 누적(reduce)하는 함수를 만들어보자.
2. reduce(List&lt;Integer&gt; list, int initial, MyReducer reducer) 형태로 구현한다.
MyReducer 는 int reduce(int a, int b); 같은 메서드를 제공하는 함수형 인터페이스이다.
initial 은 누적 계산의 초깃값(예: 0 또는 1 등)을 지정한다.
3. main() 에서 다음 연산을 테스트해보자.
연산 1: 리스트 [1, 2, 3, 4] 를 모두 더하기( + )
연산 2: 리스트 [1, 2, 3, 4] 를 모두 곱하기( * )
예시 실행
```java
리스트: [1, 2, 3, 4]
합(누적 +): 10
곱(누적 *): 24</code></pre><p>함수형 인터페이스</p>
<pre><code class="language-java">package lambda.ex2;

@FunctionalInterface
public interface MyReducer {
     int reduce(int a, int b);
}</code></pre>
<pre><code class="language-java">package lambda.ex2;

import java.util.List;

public class ReduceExample {
    // 함수를 인자로 받아, 리스트 요소를 하나로 축약(reduce)하는 고차 함수
    public static int reduce(List&lt;Integer&gt; list, int initial, MyReducer reducer) {
        // 코드 작성
        return 0; // 적절한 값으로 변경
    }

    public static void main(String[] args) {
        List&lt;Integer&gt; numbers = List.of(1, 2, 3, 4);
        System.out.println(&quot;리스트: &quot; + numbers);

        // 1. 합 구하기 (초깃값 0, 덧셈 로직)
        // 코드 작성

        // 2. 곱 구하기 (초깃값 1, 곱셈 로직)
        // 코드 작성
    }
}
</code></pre>
<p>고차 함수: MyReducer.reduce 메서드가 &quot;함수를 인자로 받아서&quot; 내부 로직(합산, 곱셈 등)을 다르게 수행한
다.
곱은 초깃값을 1로 한 것에 주의하자. 어떤 수 든지 0 을 곱하면 그 결과가 0 이 된다.
정답</p>
<pre><code class="language-java">package lambda.ex2;

import java.util.List;

public class ReduceExample {
    // 함수를 인자로 받아, 리스트 요소를 하나로 축약(reduce)하는 고차 함수
    public static int reduce(List&lt;Integer&gt; list, int initial, MyReducer reducer) {
        int result = initial;
        for (int val : list) {
            result = reducer.reduce(result, val);
        }
        return result;
    }

    public static void main(String[] args) {
        List&lt;Integer&gt; numbers = List.of(1, 2, 3, 4);
        System.out.println(&quot;리스트: &quot; + numbers);

        // 1. 합 구하기 (초깃값 0, 덧셈 로직)
        int sum = reduce(numbers, 0, (a, b) -&gt; a + b);
        System.out.println(&quot;합(누적 +): &quot; + sum);

        // 2. 곱 구하기 (초깃값 1, 곱셈 로직, 람다로 작성)
        int product = reduce(numbers, 1, (a, b) -&gt; a * b);
        System.out.println(&quot;곱(누적 *): &quot; + product);
    }
}
</code></pre>
<ul>
<li>용어 - reduce, fold</li>
<li>이렇게 여러 값을 계산해서 하나의 최종 값을 반환하는 경우 reduce(축약하다) , fold(접는다) 같은 단어를 사용한다.</li>
<li>reduce : 1, 2, 3, 4라는 숫자를 하나씩 계산하면서 축약하기 때문에 축약하다는 의미의 reduce 를 사용한다.</li>
<li>fold : 마치 종이를 여러 번 접어서 하나의 작은 뭉치로 만드는 것처럼, 초깃값과 연산을 통해 리스트의 요소를 하나씩 접어서 최종적으로 하나의 값으로 축약한다는 의미이다.</li>
</ul>
<h3 id="문제-4-함수를-반환하는-buildgreeter-만들기">문제 4. 함수를 반환하는 buildGreeter 만들기</h3>
<p>요구 사항</p>
<ol>
<li>문자열을 입력받아, 새로운 함수를 반환해주는 buildGreeter(String greeting) 라는 메서드를 작성하
자.
예) buildGreeter(&quot;Hello&quot;) &quot;Hello&quot; 를 사용하는 새로운 함수 반환
새로운 함수는 입력받은 문자열에 대해 &quot;Hello&quot;(greeting) + &quot;, &quot; + (입력받은 문자열) 형태로
결과를 반환</li>
<li>함수를 반환받은 뒤에, 실제로 그 함수를 호출해 결과를 확인해보자.
함수형 인터페이스 - 이전에 작성한 코드를 사용하자.<pre><code class="language-java">package lambda.ex2;
</code></pre>
</li>
</ol>
<p>@FunctionalInterface
public interface StringFunction {
     String apply(String s);
}</p>
<pre><code>문제 예시
```java
package lambda.ex2;

public class BuildGreeterExample {
    // 고차 함수, greeting 문자열을 받아, &quot;새로운 함수를&quot; 반환
    public static StringFunction buildGreeter(String greeting) {
        // 코드 작성
        return null; // 적절한 람다 반환
    }

    public static void main(String[] args) {
        // 코드 작성
    }
}
</code></pre><p>실행 결과</p>
<pre><code>Hello, Java
Hi, Lambda</code></pre><p>정답</p>
<pre><code class="language-java">package lambda.ex2;

public class BuildGreeterExample {
    // 고차 함수: greeting 문자열을 받아, &quot;새로운 함수를&quot; 반환
    public static StringFunction buildGreeter(String greeting) {
        // 람다로 함수 반환
        return name -&gt; greeting + &quot;, &quot; + name;
    }

    public static void main(String[] args) {
        StringFunction helloGreeter = buildGreeter(&quot;Hello&quot;);
        StringFunction hiGreeter = buildGreeter(&quot;Hi&quot;);

        // 함수가 반환되었으므로, apply()를 호출해 실제로 사용
        System.out.println(helloGreeter.apply(&quot;Java&quot;));   // Hello, Java
        System.out.println(hiGreeter.apply(&quot;Lambda&quot;));    // Hi, Lambda
    }
}
</code></pre>
<h3 id="문제-5-함수-합성하기--compose-">문제 5. 함수 합성하기 ( compose )</h3>
<p>이번에는 람다를 전달하고 또 람다를 반환까지 하는 복잡한 문제를 풀어보자.
요구 사항</p>
<ol>
<li>문자열을 변환하는 함수 두 개( MyTransformer 타입)를 받아서, f1을 먼저 적용하고, 그 결과에 f2를 적용하는
새로운 함수를 반환하는 compose 메서드를 만들어보자. 예) f2(f1(x))</li>
<li>예시 상황:
f1 : 대문자로 바꿈
f2 : 문자 앞 뒤에 &quot;<strong>&quot; 을 붙임
합성 함수( compose() )를 &quot;hello&quot; 에 적용하면 → &quot;</strong>HELLO**&quot;
함수형 인터페이스<pre><code class="language-java">package lambda.ex2;
</code></pre>
</li>
</ol>
<p>@FunctionalInterface
public interface MyTransformer {
     String transform(String s);
}</p>
<pre><code>```java
package lambda.ex2;

public class ComposeExample {
    // 고차 함수, f1, f2라는 두 함수를 인자로 받아, &quot;f1을 먼저, f2를 나중&quot;에 적용하는 새 함수 반환
    public static MyTransformer compose(MyTransformer f1, MyTransformer f2) {
        // 코드 작성
        return null; // 적절한 람다 반환
    }

    public static void main(String[] args) {
        // f1: 대문자로 변환
        MyTransformer toUpper = s -&gt; s.toUpperCase();

        // f2: 앞 뒤에 &quot;**&quot; 붙이기
        MyTransformer addDeco = s -&gt; &quot;**&quot; + s + &quot;**&quot;;

        // 합성: f1 → f2 순서로 적용하는 함수
        MyTransformer composeFunc = compose(toUpper, addDeco);

        // 실행
        String result = composeFunc.transform(&quot;hello&quot;);
        System.out.println(result); // &quot;**HELLO**&quot;
    }
}
</code></pre><p>실행 결과</p>
<pre><code>**HELLO**</code></pre><p>이번에 만나볼 고차 함수는 함수를 인자로 받아서, 또 다른 함수를 반환하는 형태이다.
힌트
문제를 풀기 쉽지 않을 것이다. compose() 메서드 안에서 MyTransformer 를 반환해야 한다. 처음에는 익명 클래
스를 사용해보자.
정답 - 익명 클래스</p>
<pre><code class="language-java">package lambda.ex2;

public class ComposeExampleEx1 {
    // 고차 함수, f1, f2라는 두 함수를 인자로 받아, &quot;f1을 먼저, f2를 나중&quot;에 적용하는 새 함수 반환
    public static MyTransformer compose(MyTransformer f1, MyTransformer f2) {
        return new MyTransformer() {
            @Override
            public String transform(String s) {
                String intermediate = f1.transform(s);
                return f2.transform(intermediate);
            }
        };
    }

    public static void main(String[] args) {
        // f1: 대문자로 변환
        MyTransformer toUpper = s -&gt; s.toUpperCase();

        // f2: 앞 뒤에 &quot;**&quot; 붙이기
        MyTransformer addDeco = s -&gt; &quot;**&quot; + s + &quot;**&quot;;

        // 합성: f1 → f2 순서로 적용하는 함수
        MyTransformer composeFunc = compose(toUpper, addDeco);

        // 테스트
        String result = composeFunc.transform(&quot;hello&quot;);
        System.out.println(result); // &quot;**HELLO**&quot;
    }
}
</code></pre>
<p>정답 - 람다</p>
<pre><code class="language-java">package lambda.ex2;

public class ComposeExampleEx2 {
    // 고차 함수, f1, f2라는 두 함수를 인자로 받아, &quot;f1을 먼저, f2를 나중&quot;에 적용하는 새 함수 반환
    public static MyTransformer compose(MyTransformer f1, MyTransformer f2) {
        return s -&gt; {
            String intermediate = f1.transform(s);
            return f2.transform(intermediate);
        };
    }

    public static void main(String[] args) {
        // f1: 대문자로 변환
        MyTransformer toUpper = s -&gt; s.toUpperCase();

        // f2: 앞 뒤에 &quot;**&quot; 붙이기
        MyTransformer addDeco = s -&gt; &quot;**&quot; + s + &quot;**&quot;;

        // 합성: f1 → f2 순서로 적용하는 함수
        MyTransformer composeFunc = compose(toUpper, addDeco);

        // 테스트
        String result = composeFunc.transform(&quot;hello&quot;);
        System.out.println(result); // &quot;**HELLO**&quot;
    }
}
</code></pre>
<p>&lt;정리&gt;
지금까지 진행한 5가지 문제는 자바에서 고차 함수를 구현할 때 자주 등장하는 패턴으로 구성되어 있다.</p>
<ol>
<li>filter: 조건(함수)을 인자로 받아, 리스트에서 필요한 요소만 추려내기</li>
<li>map: 변환 로직(함수)을 인자로 받아, 리스트의 각 요소를 다른 형태로 바꾸기</li>
<li>reduce: 누적 로직(함수)을 인자로 받아, 리스트의 모든 요소를 하나의 값으로 축약하기</li>
<li>함수를 반환: 어떤 문자열/정수 등을 받아서, 그에 맞는 새로운 &quot;함수&quot;를 만들어 돌려주기</li>
<li>함수 합성: 두 함수를 이어 붙여, 한 번에 변환 로직을 적용할 수 있는 새 함수를 만들기</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[람다 공부를 위해 알아야 할 것들]]></title>
            <link>https://velog.io/@daun_jung/%EB%9E%8C%EB%8B%A4-%EA%B3%B5%EB%B6%80%EB%A5%BC-%EC%9C%84%ED%95%B4-%EC%95%8C%EC%95%84%ED%96%90-%ED%95%A0-%EA%B2%83%EB%93%A4</link>
            <guid>https://velog.io/@daun_jung/%EB%9E%8C%EB%8B%A4-%EA%B3%B5%EB%B6%80%EB%A5%BC-%EC%9C%84%ED%95%B4-%EC%95%8C%EC%95%84%ED%96%90-%ED%95%A0-%EA%B2%83%EB%93%A4</guid>
            <pubDate>Mon, 07 Apr 2025 05:43:24 GMT</pubDate>
            <description><![CDATA[<h2 id="람다-없이-코드-조각-전달하기">람다 없이 코드 조각 전달하기</h2>
<h3 id="값-매개변수화value-parameterization">값 매개변수화(Value Parameterization)</h3>
<ul>
<li><p>문자값(Value), 숫자값(Value) 처럼 구체적인 값을 메서드 안에 두지 않고, 매개변수를 통해 외부에서 전달받는다.</p>
</li>
<li><p>재사용성을 높이는 방법이다.</p>
</li>
<li><p>값 매개변수화, 값 파라미터화 라고도 부른다.</p>
<pre><code class="language-java">public static void helloJava() {
      System.out.println(&quot;프로그램 시작&quot;);
      System.out.println(&quot;Hello Java&quot;);
      System.out.println(&quot;프로그램 종료&quot;);
  }</code></pre>
<p>값 매개변수화 후 =&gt; </p>
<pre><code class="language-java">public static void hello(String str){
      System.out.println(&quot;프로그램 시작&quot;); // 변하지 않는 부분

      // 변하는 부분 시작
      System.out.println(str);
      // 변하는 부분 종료

      System.out.println(&quot;프로그램 종료&quot;); // 변하지 않는 부분
  }
  public static void main(String[] args) {
      hello(&quot;hello Java&quot;);
  }</code></pre>
<h3 id="동작-매개변수화behavior-parameterization">동작 매개변수화(Behavior Parameterization)</h3>
</li>
<li><p>코드 조각을 메서드 안에 두지 않고 매개변수를 통해 외부에서 전달 받는다.</p>
</li>
<li><p>재사용성을 높이는 방법이다.</p>
</li>
<li><p>동작 매개변수화, 동작 파라미터화, 행동 매개변수화, 행위 파라미터화 등으로 부른다.</p>
<pre><code class="language-java">public static void helloDice() {
      long startNs = System.nanoTime();

      //코드 조각 시작
      int randomValue = new Random().nextInt(6) + 1;
      System.out.println(&quot;주사위 = &quot; + randomValue);
      //코드 조각 종료

      long endNs = System.nanoTime();
      System.out.println(&quot;실행 시간: &quot; + (endNs - startNs) + &quot;ns&quot;);
  }
  public static void main(String[] args) {
      helloDice();
  }</code></pre>
<p>동작 매개변수화 후 =&gt; </p>
<pre><code class="language-java">public static void hello(Procedure procedure){
      long startNs = System.nanoTime();

      //코드 조각 시작
      procedure.run();
      //코드 조각 종료

      long endNs = System.nanoTime();
      System.out.println(&quot;실행 시간: &quot; + (endNs - startNs) + &quot;ns&quot;);
  }

  public static void main(String[] args) {
      hello(() -&gt; {
              int randomValue = new Random().nextInt(6) + 1;
              System.out.println(&quot;주사위 = &quot; + randomValue);
      });
  }</code></pre>
</li>
</ul>
<h1 id="함수-vs-메서드">함수 VS 메서드</h1>
<h2 id="객체클래스와의-관계">객체(클래스)와의 관계</h2>
<h3 id="함수">함수</h3>
<ul>
<li>독립적으로 존재하며 클래스(객체)와 직접적인 연관이 없다.</li>
<li>절차적 언어에서는 모든 로직이 함수 단위로 구성된다.<h3 id="메서드">메서드</h3>
</li>
<li>클래스 또는 객체에 속해 있는 함수이다.</li>
<li>객체의 상태에 직접 접근하거나 객체가 제공해야 할 기능을 구현할 수 있다.</li>
<li>객체지향 언어에서 클래스 내부에 정의된 함수는 보통 메서드라고 부른다.</li>
</ul>
<h2 id="호출방식">호출방식</h2>
<h3 id="함수-1">함수</h3>
<ul>
<li>호출 시에 객체 인스턴스가 필요 없다.</li>
<li>보통 이름(매개변수) 형태로 호출된다.</li>
<li>지역 변수, 전역 변수 등과 함께 동작, 클래스나 인스턴스 변수 등은 다루지 못한다.<h3 id="메서드-1">메서드</h3>
</li>
<li>보통 객체(인스턴스).메서드이름(매개변수) 형태로 호출한다.</li>
<li>호출될 때 해당 객체의 필드나 다른 메서드에 접근 가능하다.</li>
<li>인스턴스 메서드, 정적 메서드, 추상 메서드 등 다양한 형태로 존재한다.</li>
</ul>
<p>메서드는 클래스 내부의 함수이다.
함수는 객체와 상관없이 독립적으로 호출 가능하다.
-&gt; 따라서 함수와 메서드는 수행하는 역할 자체는 같지만 소속과 호출 방식이 다르다.</p>
<h1 id="람다-맛보기">람다 맛보기</h1>
<ul>
<li>람다는 () -&gt; {} 로 표현한다.</li>
<li>람다를 사용할 때는 이름, 반환 타입은 생략, 매개변수와 본문만 적으면 된다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>