<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>j_tnqls311.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 22 Jul 2026 10:00:57 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. j_tnqls311.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/j_tnqls311" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[자바 기초 복습]]></title>
            <link>https://velog.io/@j_tnqls311/%EC%9E%90%EB%B0%94-%EA%B8%B0%EC%B4%88-%EB%B3%B5%EC%8A%B5</link>
            <guid>https://velog.io/@j_tnqls311/%EC%9E%90%EB%B0%94-%EA%B8%B0%EC%B4%88-%EB%B3%B5%EC%8A%B5</guid>
            <pubDate>Wed, 22 Jul 2026 10:00:57 GMT</pubDate>
            <description><![CDATA[<p>인프런 자바 기초강의 예전에 보고 벨로그 써놓았던거 다시 보고 복습하려고 했는데 정리가 잘 안되어있어서 programmers 기초 코드 테스트로 대체하여 진행했습니다.......😂</p>
<h4 id="문자열-붙여서-출력하기">문자열 붙여서 출력하기</h4>
<pre><code>import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        System.out.print(&quot;str1: &quot;);
        String str1 = scanner.next();

        System.out.print(&quot;str2:&quot;);
        String str2 = scanner.next();

        System.out.print(str1 + str2);
    }

}</code></pre><p>Scanner 객체 생성 sc로 되어있는데 Scanner로 써서 오류
System.out.print(&quot;str1:&quot;) 같은 안내문구 없이 그냥 값만 입력받아야 함</p>
<pre><code>import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        String str1 = sc.next();

        String str2 = sc.next();

        System.out.print(str1 + str2);
    }

}</code></pre><p>str3 = str1+str2 이런식으로 해서
Ststem.out.print(str3)도 가능</p>
<h4 id="문자열-출력하기">문자열 출력하기</h4>
<pre><code>import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String a = sc.next();

        System.out.print(a);
    }
}</code></pre><p><del>(이게 먼저였으..)</del></p>
<h4 id="a와-b-출력하기">a와 b 출력하기</h4>
<pre><code>import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int a = sc.nextInt();
        int b = sc.nextInt();

        System.out.print(&quot;a = &quot;);
        System.out.println(a);
        System.out.print(&quot;b = &quot;);
        System.out.print(b);
    }
}</code></pre><p>첨에 생각한 코드</p>
<pre><code>import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int a = sc.nextInt();
        int b = sc.nextInt();

        System.out.println(&quot;a = &quot; + a + &quot;\n&quot; + &quot;b = &quot; + b);
    }
}</code></pre><p>&quot;\n&quot;으로도 줄바꿈이 되는줄 몰랐다ㅏ.
백슬래시: \ 누르면 백슬래시로 입력됨</p>
<pre><code>import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int a = sc.nextInt();
        int b = sc.nextInt();

        System.out.println(&quot;a = &quot; + a);
        System.out.println(&quot;b = &quot; + b);
    }
}</code></pre><p>이게 내가 처음 쓴 것보다 더 편할 것 같다. </p>
<h4 id="문자열-반복해서-출력하기⭐">문자열 반복해서 출력하기⭐</h4>
<pre><code>import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String str = sc.next();
        int n = sc.nextInt();
        int number = 0;

        while (number&lt;=n){
            System.out.print(&quot;str&quot;);
            number = number + 1;
        }

        }
    }
}</code></pre><p>&quot;str&quot;으로 sout안에 써서 문자열 str 출력됌
중괄호 하나 더 씀
number = 0으로 설정해놓아서 number &lt;= n이면 6번 출력됌</p>
<pre><code>import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String str = sc.next();
        int n = sc.nextInt();
        int number = 0;

        while (number&lt;n){
            System.out.print(str);
            number ++;
        }
    }
}</code></pre><p>number ++ ; 사용하면 1씩 증가해서 편함</p>
<pre><code>import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String str = sc.next();
        int n = sc.nextInt();

        System.out.print(str.repeat(n));
    }
}</code></pre><p>.repeat(n) 사용하면 n만큼 반복</p>
<h4 id="덧셈식-출력하기">덧셈식 출력하기</h4>
<pre><code>import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int a = sc.nextInt();
        int b = sc.nextInt();
        int result = (a + b);

        System.out.println(a + &quot; + &quot; + b + &quot; = &quot; + result);
    }
}</code></pre><p>result = (a+b);에 괄호 없어도 됌</p>
<h4 id="홀짝-구분하기">홀짝 구분하기</h4>
<pre><code>import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();

        if (n%2 == 0){
            System.out.println(n + &quot; is even&quot;);
        } else{
                System.out.println(n + &quot; is odd&quot;);
        }
    }
}</code></pre><h4 id="대소문자-변환하기⭐">대소문자 변환하기⭐</h4>
<blockquote>
<h4 id="for-반복문">for 반복문</h4>
</blockquote>
<pre><code>for(초기화식; 조건식; 증감문){
    반복될 동작}</code></pre><blockquote>
<h4 id="while-반복문">while 반복문</h4>
</blockquote>
<pre><code>while(조건식){
    반복문과 증감문}</code></pre><blockquote>
<h4 id="characterisuppercase--charactertouppercase">Character.isUpperCase / CharactertoUpperCase</h4>
<p>영문의 대소문자를 구분해준다. 
Character.isUpperCase는 대문자인지 판별하고
Character.toUppercase는 대문자로 변환시킬 때 사용한다.
소문자 판별할때 Character.isLowerCase / 소문자로 바꿀때 Character.toLowerCaser</p>
</blockquote>
<blockquote>
<h4 id="문자열의-i번째-글자를-가져올때">문자열의 i번째 글자를 가져올때</h4>
</blockquote>
<pre><code>char ch = a.charAt(i)</code></pre><p>ch라는 변수에 a라는 문자열의 i번째 글자를 저장하겠다. </p>
<pre><code>import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String a = sc.next();

        for(int i = 0; i &lt; a.length(); i++){
            char ch = a.charAt(i);

            if (Character.isUpperCase(ch)){
                ch = Character.toLowerCase(ch);
                System.out.print(ch);
            } else{
                ch = Character.toUpperCase(ch);
                System.out.print(ch);
            } 
        }
    }
}</code></pre><h4 id="특수문자-쓰기⭐">특수문자 쓰기⭐</h4>
<blockquote>
<p>백슬래시1개 -&gt;     백슬래시2개
 &quot; -&gt; 백슬래시&quot;</p>
</blockquote>
<pre><code>import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {

        System.out.print(&quot;!@#$%^&amp;*(\\&#39;\&quot;&lt;&gt;?:;&quot;);
    }
}</code></pre><h4 id="문자열-90도-돌리기">문자열 90도 돌리기</h4>
<pre><code>import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String a = sc.next();

        for (int i = 0; i&lt;a.length(); i++){
            char chr = a.charAt(i);
            System.out.println(chr);
        }
    }
}</code></pre><h4 id="n의-배수">n의 배수</h4>
<pre><code>
class Solution {
    public int solution(int num, int n) {
        int answer = 0;

        if (num% n == 0){
            return answer = 1;
        } else {
            return answer = 0;
        }
    }
}</code></pre><p>이미 첨에  int answer = 0; 이라고 되어있어서 마지막에 return answer = 0; 한번 더 붙일 필요 없으</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 시큐리티 JWT]]></title>
            <link>https://velog.io/@j_tnqls311/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-JWT</link>
            <guid>https://velog.io/@j_tnqls311/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-JWT</guid>
            <pubDate>Wed, 22 Jul 2026 09:10:18 GMT</pubDate>
            <description><![CDATA[<h4 id="출처는-스프링-시큐리티-jwt개발자-유미">출처는 <a href="https://www.youtube.com/playlist?list=PLJkjrxxiBSFCcOjy0AAVGNtIa08VLk1EJ">&#39;스프링 시큐리티 JWT(개발자 유미)&#39;</a></h4>
<h2 id="1-jwt-시큐리티-동작-원리">1. JWT 시큐리티 동작 원리</h2>
<h3 id="1-로그인인증">1) 로그인(인증)</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/e58edf66-7303-4350-abe4-2476af5a57af/image.png" alt="">
로그인 요청을 받은 후 세션 방식은 서버 세션이 유저 정보를 저장하지만 JWT 방식은 토큰을 생성하여 응답한다. </p>
<p>저번에 Spring Security를 활용하여 프로그램을 만들때에는 formLogin을 사용했기 때문에 세션 로그인 방식을 활용하였다. 
하지만 이번 프로젝트에서는 JWT 방식을 활용하여 로그인을 구현해보겠다. </p>
<h3 id="2-경로-접근인가">2) 경로 접근(인가)</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/8d12dd38-b717-42e7-9f9f-b9b9a5e6c501/image.png" alt=""></p>
<p>경로 접근요청이 들어왔을 경우, JWT Filter를 통해 요청의 헤더에서 JWT를 찾아 검증한 후, 일시적 요청에 대한 세션을 생성한다. (생성된 세션은 요청이 끝나면 소멸)</p>
<blockquote>
<h4 id="jwt-경로-접근과-formlogin-경로-접근">JWT 경로 접근과 FormLogin 경로 접근</h4>
<p><strong>Form Login</strong>
JSESSIONID → 세션에서 Authentication 복원 → 인가 검사
<strong>JWT</strong>
JWT 검증 → Authentication을 매 요청마다 생성 → 인가 검사</p>
</blockquote>
<h2 id="2-기본-세팅">2. 기본 세팅</h2>
<p>스프링 이니셜라이저 -&gt; 파일 실행</p>
<p>의존성 주석처리(이니셜라이저에서 추가하고 연결하지 않으면 런타임 에러 발생)</p>
<h3 id="1-jwt-필수-의존성-세팅">1) JWT 필수 의존성 세팅</h3>
<p>JWT 토큰을 관리하기 위해 의존성을 필수로 추가해주어야 한다. 
JWT 버전은 강사님에 따라 0.12.3 버전을 사용한다.</p>
<p>build.gradle dependencies에 JWT 토큰 관리 코드를 넣길래 이것도 스프링 이니셜라이저에서 다운받을 수 있었던거 아닌가 했는데 GTP가 이거는 스프링 이니셜라이저에서 따로 다운받을 수 있었던게 아니라 이렇게 버전확인하고 복사해오는 게 맞다고 가르쳐주었다</p>
<pre><code>implementation &#39;io.jsonwebtoken:jjwt-api:0.12.3&#39;
    implementation &#39;io.jsonwebtoken:jjwt-impl:0.12.3&#39;
    implementation &#39;io.jsonwebtoken:jjwt-jackson:0.12.3&#39;</code></pre><h3 id="2-controller-작성">2) Controller 작성</h3>
<h4 id="main-controller">main controller</h4>
<pre><code>@Controller
@ResponseBody
public class MainController {

    @GetMapping(&quot;/&quot;)
    public String mainP(){

        return &quot;main controller&quot;;
    }
}</code></pre><p>mainP로 이동하게 하기 위해서 @RestController가 아닌 @Controller 어노테이션을 사용해서 main controller라는 html 파일을 찾도록 설정하였다.</p>
<p>라고 생각했는데 @Controller + @ResponseBody를 같이 쓰면 기능적으로 @RestController를 쓰는 것이랑 거의 동일하다고 한다.</p>
<blockquote>
<h4 id="controller--responsebody">@Controller + @ResponseBody</h4>
<p>@RestController와 기능적으로 거의 동일
html을 찾지않고 문자열을 그대로 반환한다. </p>
</blockquote>
<h3 id="3-security-config설정">3) Security Config(설정)</h3>
<blockquote>
<h4 id="어노테이션">어노테이션</h4>
<p>@Configuration: Config 클래스라는 것을 알림으로서 @Bean으로 등록된 객체들을 Spring이 관리할 수 있도록 한다. 
@EnableWebSecurity: SpringSecurity의 웹 보안 기능을 활성화하고 설정할 수 있도록 한다. 
-&gt; 이 클래스가 스프링 설정 클래스이며 웹 보안 설정을 담당하는 클래스라는 의미가 된다.</p>
</blockquote>
<p>그런데 요즘 업데이트된 버전은 @EnableWebSecurity가 없어도 @Bean을 통해 등록해주면 Security 기능을 쓸 수 있다고 한다. 
(명시적으로 하기 위해 여전히 쓰기도 함)</p>
<pre><code>@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // 비밀번호 암호화
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        // csrf disable
        http
                .csrf((auth) -&gt; auth.disable());

        // form 로그인 방식 disable
        http
                .formLogin((auth) -&gt; auth.disable());

        // http Basic 인증 방식 disable
        http
                .httpBasic((auth) -&gt; auth.disable());


        // 경로별 인가 작업
        http
                .authorizeHttpRequests((auth) -&gt; auth
                        .requestMatchers(&quot;/login&quot;, &quot;/&quot;, &quot;/join&quot;).permitAll()
                        .requestMatchers(&quot;/admin&quot;).hasRole(&quot;ADMIN&quot;)
                        .anyRequest().authenticated());


        // 세션 설정
        http
                .sessionManagement((session) -&gt; session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

    return http.build();
    }
}</code></pre><p>첫번째 @Bean 비밀번호 검증 및 암호화, 두번째 @Bean JWT 로그인 폼을 만들기 위한 설정변경</p>
<p>JWT를 사용하기 위해서 Spring Security에서 기본적으로 제공하는 form 로그인 방식을 비활성화하고</p>
<p>JWT를 사용하기 때문에 요청마다 아이디와 비밀번호를 헤더에 담는 인증 방식 http basic 기능을 disable했다.</p>
<p>또한 저번 Spring Security 7 수업을 들었을 때는 session을 state, 즉 기본으로 두어 따로 세션 설정을 만들지 않았는데 이번 JWT 로그인은 요청을 보낼 때마다 토큰을 따로 보내므로 로그인 정보를 저장하는 세션 설정을 stateless로 변경하였다. </p>
<h3 id="4-db-연결-및-orm">4) DB 연결 및 ORM</h3>
<blockquote>
<h4 id="ormobject-relational-mapping">ORM(Object-Relational Mapping)</h4>
<p>자바 객체와  관계형 DB 테이블을 서로 연결해 주는 기술
-&gt; 개발자가 SQL 코드를 길게 작성하지 않아도 데이터베이스 테이블과 객체를 연결해준다. </p>
</blockquote>
<p>DB는 앞서 했던 것과 동일한 방법으로 연결하였다. </p>
<h4 id="hibernate-ddl-설정--applicationproperites">Hibernate ddl 설정 : application.properites</h4>
<pre><code>spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl</code></pre><p>JPA와 hibernate가 데이터베이스를 어떻게 다룰지 정하는 코드</p>
<blockquote>
<h4 id="ddl-auto-none">ddl-auto: none</h4>
<p>create       → 실행할 때마다 기존 테이블을 지우고 새로 생성
create-drop  → 실행 시 생성, 종료 시 삭제
update       → 엔티티 구조에 맞게 테이블 수정
validate     → 엔티티와 테이블 구조가 맞는지만 검사
none         → 아무 작업도 하지 않음
.
<strong>첫 실행만 none을 create로 바꾸어 실행하고 그 뒤엔 다시 none</strong>으로 바꾼다. 
첫 실행에서 테이블을 생성했는데 계속 create로 둘 경우 테이블을 계속 지우고 새로 생성하기 때문 -&gt; 주의해서 다루어야함</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/ccbf7ce5-19c4-4220-ba9d-4db362f459fb/image.png" alt=""></p>
<p>이렇게 테이블이 잘 만들어진 것을 확인할 수 있었다.</p>
<p>또한 밑의 설정은 자바 클래스, 필드 이름을 DB 테이블, 컬럼 이름으로 바꿀 때 이름을 그대로 유지하도록 하는 설정이다.</p>
<blockquote>
<h4 id="hibernate">Hibernate</h4>
<p>자바 객체와 DB 테이블 사이에서 SQL 생성과 조회를 실제로 처리해 주는 ORM 도구,
JPA → 자바 ORM 표준 규칙
Spring Data JPA → JPA를 편하게 쓰게 해줌
Hibernate → JPA 규칙을 실제로 구현
MySQL → 실제 데이터를 저장</p>
</blockquote>
<h2 id="3-회원가입-로직-구현">3. 회원가입 로직 구현</h2>
<h3 id="1-dto">1) DTO</h3>
<h4 id="joindto">JoinDTO</h4>
<pre><code>@Getter
@Setter
public class JoinDTO {

    private String username;
    private String password;

}</code></pre><p>사용자로부터 username 값과 password 값을 받아 옮긴다.</p>
<h3 id="2-controller">2) Controller</h3>
<h4 id="joincontroller">JoinController</h4>
<pre><code>@Controller
@ResponseBody
public class JoinController {

    private final JoinService joinService;

    public JoinController(JoinService joinService) {
        this.joinService = joinService;
    }

    @PostMapping(&quot;/join&quot;)
    public String joinProcess(JoinDTO joinDTO) {
        joinService.joinProcess(joinDTO);

        return &quot;ok&quot;;
    }
}</code></pre><h3 id="3-service">3) Service</h3>
<p>UserRepository 코드 추가</p>
<pre><code>Boolean existsByUsername(String username);</code></pre><p>username이 존재하는지 o/x로 알려주는 기능을 사용한다.</p>
<h4 id="joinservice">JoinService</h4>
<pre><code>@Service
public class JoinService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public JoinService(UserRepository userRepository, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.userRepository = userRepository;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    public void joinProcess(JoinDTO joinDTO) {

        String username = joinDTO.getUsername();
        String password = joinDTO.getPassword();

        Boolean isExist = userRepository.existsByUsername(username);

        if (isExist) {

            return;
        }

        UserEntity data = new UserEntity();
        data.setUsername(username);
        data.setPassword(bCryptPasswordEncoder.encode(password));
        data.setRole(&quot;ROLE_ADMIN&quot;);
    }
}</code></pre><h2 id="4-로그인-로직-구현">4. 로그인 로직 구현</h2>
<h3 id="1-로그인-동작-원리">1) 로그인 동작 원리</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/57068dfe-01b1-497a-ac61-86195e00a651/image.png" alt="">
이번에 구현해야 할 중요한 것은 UsernamePassword AuthenticationFilter와 Authentification Manager이다.</p>
<p>저번 일반 Spring Security form Login 로직을 구현할 때는 Spring Sercurity가 이 부분을 만들어주었기 때문에 구현하지 않았지만 이번 JWT 로그인에서는 form Login 부분을 disable로 설정해두었기 때문에 커스텀을 통해 새로 구현해야 한다. </p>
<p>간단한 작동 순서로는 우선 사용자가 username과 password를 제시하면 UsernamePassword AuthenticationFilter가 username과 password를 추출하여 Authentication Manager가 DB와 매치할 수 있도록 한다. 
값이 일치한다면 successfulAuth를 반환하여 JWT 토큰을 발급한다.
만약 값이 일치하지 않는 401 응답코드를 반환한다. </p>
<h4 id="로그인-로직-구현-목표">로그인 로직 구현 목표</h4>
<ul>
<li>아이디, 비밀번호 검증을 위한 커스텀 필터 작성</li>
<li>DB에 저장되어 있는 회원 정보를 기반으로 검증할 로직 작성</li>
<li>로그인 성공시 JWT를 반환할 success 핸들러 생성</li>
<li>커스텀 필터 SecurityConfig에 등록</li>
</ul>
<h3 id="2-authenticationfilter-커스텀-구현">2) AuthenticationFilter 커스텀 구현</h3>
<h4 id="securityconfig에-코드추가">SecurityConfig에 코드추가</h4>
<pre><code>//AuthenticationManager Bean 등록
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {

        return configuration.getAuthenticationManager();
    }</code></pre><p>Spring Security가 만들어 둔 AuthenticationManager를 꺼내서 Bean으로 등록</p>
<p>AuthenticationConfiguration 안에는 authenticationManager, authenticationProvider 등 여러 authentication 관련 객체가 있고 그 설정들을 바탕으로 authenticationManager를 꺼낸 것이다. </p>
<pre><code>//필터 추가 LoginFilter()는 인자를 받음 (AuthenticationManager() 메소드에 authenticationConfiguration 객체를 넣어야 함) 따라서 등록 필요
        http
                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class);</code></pre><p>내 필터(authenticationConfiguration)를 기준필터 자리인(UsernamePasswordAuthenticationFilter)에 넣겠다.</p>
<h4 id="loginfilterjwt">LoginFilter(jwt)</h4>
<pre><code>public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    public LoginFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    // 로그인 요청이 들어왔을 때 실행되는 인증 시도 메서드
    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest request,
            HttpServletResponse response
    ) throws AuthenticationException {

        String username = obtainUsername(request);
        String password = obtainPassword(request);

        UsernamePasswordAuthenticationToken authToken =
                new UsernamePasswordAuthenticationToken(
                        username,
                        password,
                        null
                );

        return getAuthenticationManager().authenticate(authToken);
    }

    // 로그인 성공 시 JWT 발급
    @Override
    protected void successfulAuthentication(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain,
            Authentication authentication
    ) {
    }

    // 로그인 실패 처리
    @Override
    protected void unsuccessfulAuthentication(
            HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException failed
    ) {
    }
}</code></pre><p>LoginFilter가 Spring Security의 기본 로그인 필터인 UsernamePasswordAuthenticationFilter를 상속받고</p>
<p>LoginFilter가 인증을 수행하려면 AuthenticationManager가 필요하므로
super(authenticationManager)가 받은 AuthenticationManager를 부모 클래스인 UsernamePasswordAuthenticationFilter에게 전달한다.</p>
<h2 id="5-db-기반-로그인-검증로직">5. DB 기반 로그인 검증로직</h2>
<p>세션 로그인과 비슷한 방식으로 진행</p>
<h4 id="customuserdetailssevice">CustomUserDetailsSevice</h4>
<pre><code>@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //DB에서 조회
        UserEntity userData = userRepository.findByUsername(username);

        if (userData != null) {

            //UserDetails에 담아서 return하면 AutneticationManager가 검증 함
            return new CustomUserDetails(userData);
        }

        return null;
    }
}</code></pre><h4 id="customuserdetailsdto">CustomUserDetails(DTO)</h4>
<pre><code>public class CustomUserDetails implements UserDetails {

    private final UserEntity userEntity;

    public CustomUserDetails(UserEntity userEntity) {

        this.userEntity = userEntity;
    }


    @Override
    public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {

        Collection&lt;GrantedAuthority&gt; collection = new ArrayList&lt;&gt;();

        collection.add(new GrantedAuthority() {

            @Override
            public String getAuthority() {

                return userEntity.getRole();
            }
        });

        return collection;
    }

    @Override
    public String getPassword() {

        return userEntity.getPassword();
    }

    @Override
    public String getUsername() {

        return userEntity.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {

        return true;
    }

    @Override
    public boolean isAccountNonLocked() {

        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {

        return true;
    }

    @Override
    public boolean isEnabled() {

        return true;
    }
}</code></pre><p>로그인 요청이 들어오면 LoginFilter가 요청에서 username과 password를 추출하여 AuthenticationManager에 전달하고 이후 Spring Security 내부에서 UserDetailsService를 통해 데이터베이스의 사용자 정보를 조회한다.</p>
<p>CustomUserDetailsService는 Spring Security의 UserDetailsService 인터페이스를 구현한 클래스.</p>
<p>loadUserByUsername() 메서드는 로그인 요청으로 전달된 username을 이용해 UserRepository에서 사용자 정보를 조회한다. 조회된 사용자가 존재하면 UserEntity를 그대로 반환하는 것이 아니라</p>
<p>Spring Security가 이해할 수 있는 UserDetails 형태로 변환하여 반환해야 한다. 이를 위해 조회한 UserEntity를 CustomUserDetails 객체에 담아 반환한다.</p>
<p>반환된 CustomUserDetails는 저번 세션 로그인 구현 때와 동일하게 AuthenticationProvider에서 DB 비밀번호와 사용자가 로그인 요청에서 입력한 비밀번호를 비교하는데 사용된다.</p>
<p>getUsername()은 사용자의 아이디, getPassword()는 암호화 비밀번호, getAuthorities()는 권한 정보를 반환한다. </p>
<p>isAccountNonExpired(), isAccountNonLocked(), isCredentialsNonExpired(), isEnabled()는 각각 계정 만료 여부, 계정 잠금 여부, 비밀번호 만료 여부, 계정 활성화 여부를 나타낸다.</p>
<p>현재 코드에서는 모두 true를 반환하기 때문에 모든 계정이 만료X / 비밀번호 유효 / 활성화된 상태</p>
<h4 id="전체-흐름">전체 흐름</h4>
<ul>
<li>로그인 요청에서 전달된 username/password가 LoginFilter를 통해 AuthenticationManager로 전달</li>
<li>AuthenticationManager는 AuthenticationProvider에 인증 처리를 부탁</li>
<li>AuthenticationProvider는 CustomUserDetailsService의 loadUserByUsername()을 호출하여 DB에서 사용지를 조회</li>
<li>조회 결과를 CustomUserDetails 형태로 받음</li>
<li>PasswordEncoder를 이용해 입력 비밀번호와 DB의 암호화 비밀번호를 비교하며 검증에 성공하면 인증된 Authentication 객체를 생성</li>
</ul>
<h2 id="6-jwt-생성과-검증-클래스">6. JWT 생성과 검증 클래스</h2>
<p>로그인시 → 성공 → JWT 생성
접근시 → JWT 검증</p>
<p>JWT에 대해 생성과 검증을 담당할 클래스가 필요하다. 따라서 JWTUtil이라는 클래스를 생성하여 JWT 발급, 검증 메소드를 작성해야 한다. </p>
<h3 id="1-jwt-구조와-기능">1) JWT 구조와 기능</h3>
<p>JWT는 Header.Payload.Signature 구조로 이루어져 있으며
각 요소는 다음 기능을 수행한다.</p>
<h4 id="header">Header</h4>
<ul>
<li>토큰의 타입이 JWT임을 명시</li>
<li>서명에 사용된 알고리즘 정보</li>
</ul>
<h4 id="payload">Payload</h4>
<ul>
<li>사용자 정보나 권한 등의 데이터</li>
<li>비밀번호와 같은 민감한 정보는 넣으면 안됌</li>
</ul>
<h4 id="signature서명">Signature(서명)</h4>
<ul>
<li>이 토큰이 서버가 만든 게 맞는지</li>
<li>중간에 내용이 바뀌지 않았는지</li>
<li>서명알고리즘((BASE64(Header))+(BASE64(Payload)) + 비밀키)</li>
</ul>
<p>JWT는 내부 정보를 단순 BASE64 방식으로 인코딩하기 때문에 외부에서 쉽게 디코딩 할 수 있다. 
-&gt; 외부에서 열람해도 되는 정보를 담아야하며 토큰 자체의 발급처를 확인하기 위해서 사용한다.</p>
<p>(지폐와 같이 외부에서 그 금액을 확인하고 금방 외형을 따라서 만들 수 있지만 발급처에 대한 보장 및 검증은 확실하게 해야하는 경우에 사용한다. 따라서 토큰 내부에 비밀번호와 같은 값 입력 금지)</p>
<blockquote>
<h4 id="base64">BASE64</h4>
<p>누구나 쉽게 디코딩할 수 있으면 왜 인코딩하는걸까??
Base64로 인코딩한다 -&gt; 데이터를 영문자·숫자·기호로 이루어진 문자열 형태로 바꾼다
Base64를 사용하는 이유는 이미지나 파일처럼 그대로 전달하기 어려운 데이터를 문자열만 전달할 수 있는 환경에서도 안전하게 보내기 위해서</p>
</blockquote>
<p><strong>인코딩과 암호화는 같은 뜻이 아님</strong>
인코딩: 데이터의 형태를 바꾸겠다
암호화: 외부에서 값을 알아볼 수 없도록 바꾸겠다.</p>
<h3 id="2-jwt-서명-알고리즘signature">2) JWT 서명 알고리즘(Signature)</h3>
<p>비밀키 -  서버만 알고 있는 비밀번호 같은 문자열</p>
<blockquote>
<h4 id="서명">서명</h4>
</blockquote>
<ul>
<li>대칭키 기반 서명<ul>
<li>HS256</li>
<li>하나의 비밀키로 서명 생성 및 검증
.</li>
</ul>
</li>
<li>비대칭키 기반 서명<ul>
<li>RS256</li>
<li>개인키로 서명 생성, 공개키로 검증</li>
</ul>
</li>
</ul>
<p>-&gt; 서명을 만들 때 쓰는 키와 검증할 때 쓰는 키가 같냐, 다르냐</p>
<p>JWT 기반 로그인의 중요한 점은 데이터를 암호화하는 것이 아닌 위조되지 않았음을 확인시키는 것</p>
<blockquote>
<p>Payload에 있는 값 + 비밀키 -&gt; signature 생성
만약 Payload에 있는 값이 달라지면 signature이 달라지기 때문에 토큰을 믿을 수 없다고 판단한다. </p>
</blockquote>
<h3 id="3-암호화-키-저장">3) 암호화 키 저장</h3>
<h4 id="applicationproperties">application.properties</h4>
<p>코드 추가</p>
<pre><code>spring.jwt.secret=vmfhaltmskdlstkfkdgodyroqkfwkdbalroqkfwkdbalaaaaaaaaaaaaaaaabbbbb</code></pre><p>여기서는 그냥 적었지만 암호화 키는 구현 내부에 탑재하는 것(하드코딩)을 지양하기 때문에 환경변수 이름만 적어두고 .gitignore 파일에 저장해야 한다. </p>
<h3 id="4-jwtutil">4) JWTUtil</h3>
<p>-&gt; JWT 생성과 검증 클래스 </p>
<h4 id="토큰-payload에-저장될-정보">토큰 Payload에 저장될 정보</h4>
<ul>
<li>username</li>
<li>role</li>
<li>생성일</li>
<li>만료일</li>
</ul>
<h4 id="jwtutil-구현-메소드">JWTUtil 구현 메소드</h4>
<ul>
<li>JWTUtil 생성자(JWT 서명에 사용할 비밀키를 준비하는 것)</li>
<li>username 확인 메소드</li>
<li>role 확인 메소드</li>
<li>만료일 확인 메소드</li>
</ul>
<pre><code>@Component
public class JWTUtil {

    // 비밀키 객체 생성
    private SecretKey secretKey;

    // 생성자 실행
    public JWTUtil(@Value(&quot;${spring.jwt.secret}&quot;)String secret) {

        secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }

    // JWT 정보조회(username 조회)
    public String getUsername(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get(&quot;username&quot;, String.class);
    }

    // JWT 정보조회(role 조회)
    public String getRole(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get(&quot;role&quot;, String.class);
    }

    // JWT 정보조회(만료여부 조회)
    public Boolean isExpired(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
    }

    // 모든 검사가 통과되면 builder에게 반환하여 JWT 발급
    public String createJwt(String username, String role, Long expiredMs) {

        return Jwts.builder()
                .claim(&quot;username&quot;, username)
                .claim(&quot;role&quot;, role)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + expiredMs))
                .signWith(secretKey)
                .compact();
    }
}</code></pre><pre><code>// 생성자 실행
    public JWTUtil(@Value(&quot;${spring.jwt.secret}&quot;)String secret) {

        secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
    }</code></pre><p>생성자가 실행되면 application.properties에서 Spring.jwt.secret 값을 가져온다. JWT 객체를 만든다.</p>
<p>이 값을 사용해서 비밀키를 만든다.
앞서 받은 String secret 변수를 바이트로 나눠준다. 
HS256 알고리즘을 가져와서 secretKey를 생성한다.</p>
<pre><code>// JWT 정보조회(username 조회)
    public String getUsername(String token) {

        return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get(&quot;username&quot;, String.class);
    }</code></pre><p>paser()는 Jwt를 받아서 해석하는 도구를 만들겠다는 뜻
.parseSignedClaims(token) 해석을 수행</p>
<ul>
<li>JWT 형식이 올바른지</li>
<li>서명이 올바른지</li>
<li>토큰 구조에 문제가 없는지</li>
</ul>
<h3 id="5-로그인-성공과-실패">5) 로그인 성공과 실패</h3>
<pre><code>// JWTUtill 주입
    private final JWTUtil jwtUtil;

    public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil) {
        this.authenticationConfiguration = authenticationConfiguration;
        this.jwtUtil = jwtUtil;
    }
</code></pre><pre><code>//AuthenticationManager()와 JWTUtil 인수 전달
        http
                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);

        return http.build();</code></pre><p>직접 만든 LoginFilter를 기존의 UsernamePasswordAuthenticationFilter 위치에 등록하고 LoginFilter에 authenticationManager(username/password 검증)와 JWTuUtil(JWT 토큰 발급)을 받아온다.</p>
<h1 id="어려워"><del>(어려워)</del></h1>
<h2 id="7-jwt-검증-필터">7. JWT 검증 필터</h2>
<p>스프링 시큐리티 filterChain에 요청에 담긴 JWT를 검증하기 위한 커스텀 필터를 등록해야 한다.</p>
<p>해당 필터를 통해 요청 헤더 Authorization 키에 JWT가 존재하는 경우 JWT를 검증하고 SecurityContextHolder에 세션을 생성한다. (이 세션은 STATLESS 상태로 관리되기 때문에 해당 요청이 끝나면 소멸된다.)</p>
<h4 id="jwtfilter">JWTFilter</h4>
<pre><code>public class JWTFilter extends OncePerRequestFilter {

    private final JWTUtil jwtUtil;

    public JWTFilter(JWTUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        //request에서 Authorization 헤더를 찾음
        String authorization= request.getHeader(&quot;Authorization&quot;);

        //Authorization 헤더 검증
        if (authorization == null || !authorization.startsWith(&quot;Bearer &quot;)) {

            System.out.println(&quot;token null&quot;);
            filterChain.doFilter(request, response);

            //조건이 해당되면 메소드 종료 (필수)
            return;
        }

        System.out.println(&quot;authorization now&quot;);
        //Bearer 부분 제거 후 순수 토큰만 획득
        String token = authorization.split(&quot; &quot;)[1];

        //토큰 소멸 시간 검증
        if (jwtUtil.isExpired(token)) {

            System.out.println(&quot;token expired&quot;);
            filterChain.doFilter(request, response);

            //조건이 해당되면 메소드 종료 (필수)
            return;
        }

        //토큰에서 username과 role 획득
        String username = jwtUtil.getUsername(token);
        String role = jwtUtil.getRole(token);

        //userEntity를 생성하여 값 set
        UserEntity userEntity = new UserEntity();
        userEntity.setUsername(username);
        userEntity.setPassword(&quot;temppassword&quot;);
        userEntity.setRole(role);

        //UserDetails에 회원 정보 객체 담기
        CustomUserDetails customUserDetails = new CustomUserDetails(userEntity);

        //스프링 시큐리티 인증 토큰 생성
        Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());

        // 현재 요청의 SecurityContext에 인증 정보 등록
        SecurityContextHolder.getContext().setAuthentication(authToken);

        filterChain.doFilter(request, response);
    }
}</code></pre><p>요청에서 JWT 찾기 → JWT 검증 → 사용자 정보 꺼내기 → Authentication 생성 → SecurityContext에 저장 → 다음 필터로 넘기기</p>
<pre><code>public class JWTFilter extends OncePerRequestFilter {</code></pre><p>모든 요청마다 한번씩 실행되는 필터 상속받기
사용자가 요청을 보내면 JWTFilter가 중간에 JWT 토큰을 검사한다.</p>
<pre><code>private final JWTUtil jwtUtil;

public JWTFilter(JWTUtil jwtUtil) {
    this.jwtUtil = jwtUtil;
}</code></pre><p>JWTUtil을 사용하여 토큰 해석, 서명 검증, 만료 확인 등 JWT를 검사한다.</p>
<p>그 후 @Override를 사용하여 사용자가 보낸 요청(Request)과 보낼 응답(Response), 검증(filterChain)을 수행하는 메서드를 만든다. </p>
<p>Request에서 header에 Bearer 형식의 토큰이 있는지 검사하고 Bearer 부분 제거 후 순수 토큰만을 얻어 JWTUtil에서 검증한다. </p>
<p>검증이 완료되면 username과 role을 얻어 userEntity를 생성하여 정보를 담는다. new UserEntity이므로 DB에 저장되는 정보는 아니고 임시적으로 로그인 되었음을 알리기 위한 Entity이다.</p>
<h4 id="securityconfig">SecurityConfig</h4>
<p>코드추가</p>
<pre><code>//JWTFilter 등록
        http
                .addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);</code></pre><p>jWTFilter가 LonginFilter보다 먼저 실행될 수 있도록 SecurityConfig에 설정을 추가해준다. </p>
<h2 id="8-세션-정보">8. 세션 정보</h2>
<h4 id="main-controller-1">Main Controller</h4>
<pre><code>@Controller
@ResponseBody
public class MainController {

    @GetMapping(&quot;/&quot;)
    public String mainP() {

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        String name = SecurityContextHolder.getContext().getAuthentication().getName();

        Collection&lt;? extends GrantedAuthority&gt; authorities = authentication.getAuthorities();
        Iterator&lt;? extends GrantedAuthority&gt; iter = authorities.iterator();
        GrantedAuthority auth = iter.next();
        String role = auth.getAuthority();

        return &quot;username : &quot; + name + &quot;, role : &quot; + role;
    }
}</code></pre><p>앞에서 JWTFilter가 정상적인 JWT를 확인하면 그러면 Spring Security의 SecurityContext 안에 현재 요청 사용자의 인증 정보가 저장된다. Authentication 안에 CustomUserDetails가 들어 있으므로 getName()은 CustomUserDetails.getUsername() 값을 반환한다.</p>
<h2 id="9-cors-설정">9. CORS 설정</h2>
<blockquote>
<h4 id="cors란">CORS란?</h4>
<p>Cross-origin resource sharing = 교차 출처 리소스 공유
브라우저가 다른 출처의 서버로 요청을 보낼 때 적용하는 보안 규칙
허용하는 서버의 요청을 지정해주어야 한다. 
ex) 프론트엔드와 백엔드의 실행 서버가 다르기 때문에 다른 출처라고 본다.</p>
</blockquote>
<h4 id="springconfig">SpringConfig</h4>
<p>코드추가</p>
<pre><code>http
                .cors((corsCustomizer -&gt; corsCustomizer.configurationSource(new CorsConfigurationSource() {

                    @Override
                    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {

                        CorsConfiguration configuration = new CorsConfiguration();

                        configuration.setAllowedOrigins(Collections.singletonList(&quot;http://localhost:3000&quot;));
                        configuration.setAllowedMethods(Collections.singletonList(&quot;*&quot;));
                        configuration.setAllowCredentials(true);
                        configuration.setAllowedHeaders(Collections.singletonList(&quot;*&quot;));
                        configuration.setMaxAge(3600L);

                        configuration.setExposedHeaders(Collections.singletonList(&quot;Authorization&quot;));

                        return configuration;
                    }
                })));</code></pre><h4 id="corsmvcconfig">CorsMvcconfig</h4>
<p>&lt; Config</p>
<pre><code>@Configuration
public class CorsMvcConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry corsRegistry) {

        corsRegistry.addMapping(&quot;/**&quot;)
                .allowedOrigins(&quot;http://localhost:3000&quot;);
    }
}</code></pre><p>요청을 허용할 프론트의 경로 입력</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 시큐리티 7]]></title>
            <link>https://velog.io/@j_tnqls311/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-7</link>
            <guid>https://velog.io/@j_tnqls311/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-7</guid>
            <pubDate>Sun, 19 Jul 2026 16:29:23 GMT</pubDate>
            <description><![CDATA[<h3 id="모든-사진의-출처는-개발자유미스프링-시큐리티-7">모~든 사진의 출처는 <a href="https://www.youtube.com/playlist?list=PLJkjrxxiBSFA27BZXMhX_wn3gmrUJW4cQ">&quot;개발자유미(스프링 시큐리티 7)&quot;</a></h3>
<h4 id="스프링-시큐리티-db-기반-로그인-시스템-구현">스프링 시큐리티 DB 기반 로그인 시스템 구현</h4>
<ul>
<li>username / password 기반 로그인</li>
<li>username / password는 DB 저장(MySQL과 JPA 활용)</li>
<li>세션 방식 + SSR</li>
<li>유저 권한(role) 별 경로에 따른 인가 설정</li>
</ul>
<blockquote>
<h4 id="ssr이란">SSR이란?</h4>
<p><strong>Server-Side Rendering(서버 사이드 렌더링)</strong>의 약자
-&gt; 서버가 완성된 HTML 화면을 만들어서 브라우저에 보내주는 방식.
지금처럼 Spring Boot에서 Thymeleaf 또는 Mustache를 사용한다면 SSR 방식이고 React처럼 서버가 주로 JSON 데이터만 주고 브라우저가 화면을 만드는 방식은 보통 <strong>CSR(Client-Side Rendering)</strong>이라고 한다. </p>
</blockquote>
<h2 id="1-시스템-모식도">1. 시스템 모식도</h2>
<h4 id="스프링-시큐리티가-없을-경우-시스템-모식도">스프링 시큐리티가 없을 경우 시스템 모식도</h4>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/14517be2-88a3-47a3-927b-2b4dc4817d7b/image.png" alt="">
아무런 인가설정이 되어있지 않기 때문에 전역에 접근이 가능하다. </p>
<h4 id="스프링-시큐리티의존성security-config가-적용된-후-시스템-모식도">스프링 시큐리티(의존성/security config)가 적용된 후 시스템 모식도</h4>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/9a671bb0-6906-48d5-a0d8-91e15ade0c6a/image.png" alt="">
의존성과 SecurityConfig 설정으로 시큐리티 필터라는 베이스 클래스가 자동으로 추가된다. 여기에 username과 password 기반의 로그인 시스템과 인가 코드만 부여하면 쉽게 로그인 구현이 가능하다.</p>
<blockquote>
<h4 id="톰캣이란">톰캣이란?</h4>
<p>스프링부트 3을 공부할때부터 스프링부트와 코드가 톰캣과 함께 작동한다고 들었다. 톰캣이 정확히 무엇일까?
: 자바로 만든 웹 애플리케이션을 실행하고 클라이언트의 HTTP 요청을 처리하는 웹 서버이자 서블릿 컨테이너
Postman -&gt; 톰캣(요청 주소, HTTP 메서드 등 확인) -&gt; 스프링 부트에 요청 전달 -&gt; DispatcherServlet -&gt; Controller -&gt; 톰캣(HTTP 응답)</p>
</blockquote>
<blockquote>
<h4 id="서블릿servlet">서블릿(Servlet)</h4>
<p>HTTP 요청을 받아서 응답을 만드는 자바 프로그램
Spring을 사용하면 알아서 다 만들어서 관리됀다. 
DispatcherServlet은 스프링 MVC에서 모든 요청을 가장 먼저 받아서 적절한 컨트롤러로 보내는 역할 수행</p>
</blockquote>
<blockquote>
<h4 id="mvc">MVC</h4>
<p>MVC는 Model–View–Controller의 약자
웹 애플리케이션의 역할을 세 부분으로 나눠서 관리하는 구조</p>
</blockquote>
<p>-&gt; 위 부분들에 대해서는 추후 더 공부해보도록 하겠다. </p>
<h2 id="2-프로젝트-생성--데이터베이스-연결">2. 프로젝트 생성 &amp; 데이터베이스 연결</h2>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/f0d2d8ed-82c3-41c2-9aca-e370e98c7aa5/image.png" alt=""></p>
<p>데이터베이스 설정을 하지 않고 스프링만 연동한 채 실행을 누르면 에러가 뜨고 실행이 되지 않는다.
따라서 White Label Error Page를 보기 위해서일지라도 데이터베이스 연결은 필수적으로 해야한다. </p>
<h3 id="1-db-설정">1) DB 설정</h3>
<p>DB 연결을 위해 application.properties에 코드를 추가한다.</p>
<pre><code># MySQL DB connection
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://아이피:306/논리디비이름?useSSL=false&amp;useUnicode=true&amp;serverTimezone=Asia/Seoul&amp;allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=비밀번호

# JPA ddl, Naming and log
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.show-sql=true
]</code></pre><h3 id="2-테스트용-controller-만들기">2) 테스트용 controller 만들기</h3>
<pre><code>@Controller
public class SampleController {

    @GetMapping(&quot;/&quot;)
    public String index(){
        return &quot;index&quot;;
    }

    @GetMapping(&quot;/user&quot;)
    public String user() {
        return &quot;user&quot;;
    }

    @GetMapping(&quot;admin&quot;)
    public String admin(){
        return &quot;admin&quot;;
    }
}</code></pre><p>@GetMapping 어노테이션을 활용하여 사용자가 보낸 각 경로의 GET 요청을 각각의 컨트롤러 메서드와 연결한다.</p>
<blockquote>
<h4 id="controller-vs-restcontroller">@Controller vs @RestController</h4>
<p><strong>@Controller</strong>는 화면을 반환할때 주로 사용한다. 위에서 사용한 &quot;index&quot;의 값은 index 문자열을 가리키는 것이 아닌 index html 화면을 가리키는 것이다. 
<strong>@RestController</strong>는 데이터값을 바로 반환할 때 사용한다. 위 상황에서 @RestController를 썼다면 실행했을때 스프링이 객체를 JSON으로 변환해서 HTTP 응답 본문에 넣었기 때문에 &quot;index&quot;라는 문자열을 그대로 반환했을 것이다. </p>
</blockquote>
<p>이 프로젝트에서는 일반 @Controller를 사용하였기 때문에 각 entity에 따른 html을 생성해주었다.</p>
<blockquote>
<h4 id="requestmapping와-getmapping">@RequestMapping(&quot;/&quot;)와 @GetMapping(&quot;/&quot;)</h4>
<p>(&quot;&quot;) 속에 /는 URL 경로를 의미한다. 
@GetMapping은 GET 요청을 받을 URL 경로이고 
@RequestMapping은 클래스의 공통 URL 경로를 지정하여 비슷한 기능끼리 묶을 수 있게 한다. </p>
</blockquote>
<h2 id="3-securityconfig-등록">3. SecurityConfig 등록</h2>
<p>이 클래스 내부에 시큐리티 코드를 통제할 @Bean들을 등록한다. </p>
<p>@Bean은 개발자가 만든 객체를 스프링이 관리하도록 등록하는 어노테이션</p>
<h3 id="1-시큐리티-필터-통제">1) 시큐리티 필터 통제</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/9a671bb0-6906-48d5-a0d8-91e15ade0c6a/image.png" alt="">
시큐리티 필터는 여러개의 필터로 이루어진다. 보안 검증, 로그아웃, 로그인, 세션관리, 인가 등등..</p>
<h4 id="비밀번호-암호화">비밀번호 암호화</h4>
<pre><code>@Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }</code></pre><p>DB가 유출되었을 경우 비밀번호를 알 수 없도록 법으로 지정되어있다. 
따라서 암호화를 할 수 있는 PassordEncoder를 등록하여 BCrypt를 사용한다. (일반적으로 많이 사용하는 암호화 방법)</p>
<h2 id="4-회원가입">4. 회원가입</h2>
<p>로그인 시 유저가 로그인 요청시 입력한 username/password와 DB에 저장되어있는 username/password를 비교해야 한다.</p>
<p>이를 위해서는 미리 DB에 유저 정보를 저장해야 한다. </p>
<h4 id="userentity">UserEntity</h4>
<pre><code>@Entity
@Getter
@Setter
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String username;
    private String password;

    @Enumerated(EnumType.STRING)
    private UserRole role;

}</code></pre><h4 id="userrole">UserRole</h4>
<pre><code>public enum UserRole {

    ADMIN,
    USER
}</code></pre><h4 id="userservice">UserService</h4>
<pre><code>@Service
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder){
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    // 회원가입 메소드
    public void join(UserRequestDTO dto){
        String username = dto.getUsername();
        String password = dto.getPassword();

        UserEntity entity = new UserEntity();
        entity.setUsername(username);
        entity.setPassword(passwordEncoder.encode(password));
        entity.setRole(UserRole.USER);

        userRepository.save(entity);
    }

}</code></pre><p>이미 동일한 유저네임이 있는지 체크하는 기능 또는 성공하면 리턴값을 주는 기능 등으로 디테일과 에러에 신경쓸 수 있다.</p>
<h4 id="userdto">userDTO</h4>
<pre><code>@Setter
@Getter
public class UserRequestDTO {

    private String username;
    private String password;

}</code></pre><h4 id="joincontroller">JoinController</h4>
<pre><code>@Controller
public class JoinController {

    private final UserService userService;

    public JoinController(UserService userService){
        this.userService = userService;
    }

    // 회원 가입 데이터 처리 메소드
    @PostMapping
    public String join(UserRequestDTO dto){
        userService.join(dto);

        return &quot;redirect:/&quot;;
    }


    // 회원 가입 페이지 제공 메소드
    @GetMapping(&quot;/join&quot;)
    public String joinPage(){
        return &quot;join&quot;;
    }
}</code></pre><p>&quot;redirect:&quot;의 경우 현재 인텔리제이에서 html까지 만들어 보내므로 사용한 것이고 프론트와 협업할 때는 프론트가 그 부분을 맡아주기 때문에 크게 신경쓰지 않아도 된다. </p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/3699fd91-ab90-443c-b6fc-b82678ba15e9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/ba286069-2546-4a64-80e5-105e89681546/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/4dfceac3-047b-4dfa-9540-45a9376eed02/image.png" alt="">
password 같은 경우 암호화가 잘 되어서 저장된 것을 확인할 수 있다. </p>
<h2 id="5-로그인">5. 로그인</h2>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/a6ea6870-1bd1-4869-9df4-a2d080d588b0/image.png" alt="">
파란색이 앞쪽 / 빨간색이 뒤쪽</p>
<h3 id="1-앞쪽유저-로그인-페이지">1) 앞쪽(유저 로그인 페이지)</h3>
<p>로그인 페이지 -&gt; 아이디/비밀번호 -&gt; 시큐리티 필터 -&gt; AuthenticationManager -&gt; AuthenticationProvider&#39;</p>
<p>어센티케이션이라고.. 읽는다.</p>
<p>SecurityConfig 클래스 및 스프링 시큐리티 의존성 기본 활성화 값을 활용하여 위 구성을 다 직접 구현하지 않고 일부만을 설정하겠다.</p>
<blockquote>
<h4 id="authenticationmanager-vs-authenticationprovider">AuthenticationManager vs AuthenticationProvider</h4>
<p><strong>AuthenticationManager</strong>  - 인증 요청을 받아서 누가(Contoroller) 실제로 인증을 처리할지 정하는 역할
<strong>AuthenticationProvider</strong> - 실제 인증을 수행, DB에서 사용자를 찾고 PasswordEncoder로 비밀번호가 맞는지 검사한다.</p>
</blockquote>
<h4 id="logincontroller">LoginController</h4>
<pre><code>@Controller
public class LoginController {

    @GetMapping
    public String loginPage(){
        return &quot;longin&quot;;
    }
}</code></pre><p>그냥 @Controller 사용하였으니 login html 페이지를 만들어 반환할 수 있도록 한다.</p>
<h4 id="securityconfig">SecurityConfig</h4>
<pre><code>// 로그인 필터 설정
        http
                .formLogin(login -&gt; login
                        .loginProcessingUrl(&quot;/login&quot;)
                        .loginPage(&quot;login&quot;));</code></pre><p>이 설정을 통해 여러가지의 시큐리티 필터 중 로그인 요청을 만나면 로그인 필터를 거칠 수 있게 된다. </p>
<h3 id="2-뒤쪽db">2) 뒤쪽(DB)</h3>
<p>DB -&gt; Repository -&gt; Service -&gt; AuthenticationProvider -&gt;  AuthenticationManager</p>
<p>직접 DB 조회 부분을 구현 후 Service 단에서 규칙을 맞춰주면  AuthenticationProvider가 데이터를 불러서 사용한다.</p>
<h4 id="service-단-수정">Service 단 수정</h4>
<pre><code>@Service
public class UserService implements UserDetailsService {</code></pre><p>UserDetailsServie라는 클래스를 만들겠다는 의미인데 아래에 사용이 안되고 있으므로 이것만 작성하면 오류가 나타난다.</p>
<p>따라서 상단 오류를 누르고 메소드 구현을 클릭하면 </p>
<pre><code>@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return null;
    }</code></pre><p>자동으로 이런식의 구현을 해준다.
이 코드가 곧 AuthenticationProvider를 불러서 쓸 메소드이다. </p>
<pre><code>@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        UserEntity entity = userRepository.findByUsername(username).orElseThrow();

        return User.builder()
                .username(entity.getUsername())
                .password(entity.getPassword())
                .roles(entity.getRole().name())
                .build();
    }</code></pre><p>자동생성된 코드 안에 Repository에서 username 기반으로 정보를 얻어  username과 암호화된 password, role을 User에게 반환해준다.
이때의 User는 내가 생성한 UserEntity가 아닌 Form 로그인 사용시 자동으로 생성되는 엔티티이다. </p>
<blockquote>
<h4 id="override⭐">@Override⭐</h4>
<p>: 부모 클래스나 인터페이스에 이미 정의된 메서드를 자식 클래스에서 다시 구현했다는 표시
부모 클래스를 상속받은 자식 클래스는 부모 클래스를 가져와서 쓸 수 있다. 또한 overload를 사용하면 객체에 대해 자신의 클래스에서 재정의할 수 있으므로 변형한 형태로도 쓸 수 있다. </p>
</blockquote>
<h2 id="6-인가-및-유저-정보">6. 인가 및 유저 정보</h2>
<p>세션 방식이기 때문에 로그인에 성공하면 서버가 유저를 기억하게 된다. 따라서 매 접속시 해당 유저인지 알 수 있는 것이다. 따라서 유저를 추가적으로 기억하는 메커니즘은 구현하지 않아도 된다. </p>
<h3 id="1-유저-정보">1) 유저 정보</h3>
<p>SampleController</p>
<pre><code>@GetMapping(&quot;/&quot;)
    public String index(){

        String username= SecurityContextHolder.getContext().getAuthentication().getName();
        String role = SecurityContextHolder.getContext().getAuthentication().getAuthorities().toString();

        System.out.println(username);
        System.out.println(role);

        return &quot;index&quot;;
    }</code></pre><h3 id="2-인가">2) 인가</h3>
<p>경로별 접근하는 유저에 대한 접근 허용과 거부 설정</p>
<p>인가 단위</p>
<ul>
<li>role별: 큰 그룹에 대해(admin, role)</li>
<li>같은 그룹별: 같은 user 그룹이라고 하더라도 username별로 접근을 다르게 한다. </li>
</ul>
<p>필터 쪽 인가 -&gt; role 별
컨트롤러쪽 인가: User에서 username 별</p>
<h4 id="securityconfig-1">SecurityConfig</h4>
<pre><code>// 인가 필터에 대한 설정
        http
                .authorizeHttpRequests(auth -&gt;auth
                        .requestMatchers(&quot;/&quot;).permitAll()
                        .requestMatchers(&quot;/join&quot;).permitAll()
                        .requestMatchers(&quot;/login&quot;).permitAll()
                        .requestMatchers(&quot;/user&quot;).hasRole(&quot;USER&quot;,&quot;ADMIN&quot;)
                        .requestMatchers(&quot;/admin&quot;).hasAnyRole(&quot;ADMIN&quot;)
                        .anyRequest().denyAll()
                );</code></pre><p>메인 페이지 -&gt; 모든 접근 허용
회원가입 -&gt; 모든 접근 허용
로그인 -&gt; 모든 접근 허용
유저 페이지 -&gt; user 권한을 가진 사람만 허용
admin 페이지 -&gt; admin 권한을 가진 사람만 허용
이외의 경로 -&gt; 모두 거부</p>
<blockquote>
<h4 id="기타-메소드별로-나누는-방법">기타: 메소드별로 나누는 방법</h4>
</blockquote>
<pre><code>.requestMatchers(Http.GET,&quot;/&quot;).permitAll();</code></pre><p>메인 페이지로 오는 GET 요청만 모두 허용하겠다.  </p>
<h4 id="service-또는-component">Service 또는 Component</h4>
<pre><code>public boolean authCheck(Long postId) {

    String username = SecurityContextHolder.getContext()
            .getAuthentication()
            .getName();

    // 커스텀 쿼리
    return postRepository.existsIdAndUsernameCustom(postId, username);
}</code></pre><p>username별로 자신의 글만 삭제나 수정할 수 있다던지 하는 권한 검사 메소드 등록</p>
<h2 id="7-csrf-enable">7. CSRF enable</h2>
<h3 id="1--csrf">1)  CSRF</h3>
<blockquote>
<h4 id="csrf가-정확히-뭘까">CSRF가 정확히 뭘까?</h4>
<p>공격자가 사용자의 비밀번호를 알아내는 것이 아니라 이미 로그인된 사용자의 브라우저, 즉 그 안에 저장된 쿠키를 이용해 요청을 대신 보내는 공격</p>
</blockquote>
<blockquote>
<p><strong>정상 세션 로그인 방법</strong>
유저 로그인 -&gt; 서버에서 세션 생성 -&gt; 브라우저가 세션 쿠키를 받음 -&gt; 서버에 접속하고 요청을 보낼때마다 쿠키를 &quot;자동&quot; 전달 -&gt; 이것으로 사용자 판별</p>
</blockquote>
<blockquote>
<p><strong>CSRF 공격이 가능한 이유</strong>
-&gt; 예를 들어 사용자가 은행 사이트에 로그인한 상태라면
사용자의 브라우저에는 은행 로그인 세션 쿠키가 저장되어 있다.
이 상태에서 사용자가 공격자의 사이트에 접속할 경우
공격자의 사이트에는 사용자가 송금 버튼을 누르지 않았지만 은행 서버로 송금 요청을 보내는 코드가 써있을 수 있다.
이때 브라우저는 은행 사이트로 요청을 보내면서 은행의 세션 쿠키를 자동으로 포함할 수 있으며 이 때문에 은행은 이 요청이 사용자의 요청인줄 착각하게 된다. </p>
</blockquote>
<h3 id="2-csrf-토큰">2) CSRF 토큰</h3>
<p>이것을 막기 위한 방법으로 <strong>CSRF 토큰</strong>이라는 임의의 값을 사용한다.</p>
<p>사용자가 HTML 페이지를 응답할 때 서버는 사용자에게 CSRF 토큰을 함께 넣는다.
정상적인 사용자가 이 폼을 제출하면 쿠키와 CSRF 토큰 값이 함께 전송된다.
서버는 요청을 처리하기 전에 쿠키와 CSRF 토큰 값이 모두 올바른지 확인한다.</p>
<blockquote>
<h4 id="csrf-토큰이-요청-위조를-막는-이유">CSRF 토큰이 요청 위조를 막는 이유</h4>
<p>세션 쿠키는 브라우저가 자동으로 전송하지만
CSRF 토큰은 정상 서버가 제공한 HTML 페이지 안에 포함된 별도의 값이다.
따라서 서버는 CSRF 토큰을 확인함으로써 해당 요청이 정상 서버에서 제공한 폼을 통해 만들어진 요청인지 검사할 수 있다.</p>
</blockquote>
<p>CSRF 필터를 사용하게 되면 앞으로 서버의 상태를 바꾸는 요청은 모두 CSRF 토큰을 담아서 보내야 한다. 
-&gt; 몇가지 경로는 예외하도록 설정 가능</p>
<pre><code>http
                .csrf(csrf -&gt; csrf
                        .ignoringRequestMatchers(&quot;/logout&quot;));</code></pre><p>/logout 요청에 대해서는 CSRF 토큰 검사를 하지 않도록 예외 처리
CSRF 설정시 로그아웃은 무조건 POST 요청(서버의 상태변경)만 허용되기 때문에 위 설정을 추가하기도 한다. </p>
<h2 id="8-remember-me로그인-유지">8. Remember me(로그인 유지)</h2>
<p>로그인 상태 유지를 위한 기능
일반 로그인은 세션이 살아있는 짧은 기간만 로그인이 유지되지만 리멤버미는 오랜 시간 후 접속해도 로그인 유지가 가능하다.
ex) 네이버의 로그인 유지기능</p>
<h3 id="1-spring-remember-me">1) Spring Remember me</h3>
<ol>
<li>오래 유지되는 Remember Me 쿠키 발급 방식</li>
<li>영속성(DB) 사용 방식의 리멤버미</li>
</ol>
<blockquote>
<h4 id="영속성이란">영속성이란?</h4>
<p>프로그램이나 서버가 꺼져도 데이터가 사라지지 않고 계속 남아 있는 성질.
예시로 데이터 값을 변수에만 저장하게 되면 프로그램이 종료되는 순간 사라진다. 하지만 DB에 값을 저장해두면 서버가 꺼지더라도 데이터는 유지된다.
Remember me에서 말하는 영속성 사용 방식은 주로 자동 로그인용 토큰을 DB에 저장하여 사라지지 않게 함을 의미한다. </p>
</blockquote>
<p>이번에는 쿠키 방식으로 구현한다. </p>
<p>Security Config</p>
<pre><code>// remember me 설정
        http
                .rememberMe(me -&gt; me
                        .key(&quot;vmfhaltmskdlsvmfhaltmskdlsvmfhaltmskdls&quot;)
                        .rememberMeParameter(&quot;remember-me&quot;)
                        .tokenValiditySeconds(14*24*60*60));</code></pre><p>.tokenValiditySeconds(14<em>24</em>60*60)은 Remember Me 쿠키의 유효기간을 14일로 설정하는 것을 의미한다. 
14일 × 24시간 × 60분 × 60초로 계산</p>
<blockquote>
<h4 id="key-값-설정하는-방법">key 값 설정하는 방법</h4>
<p>랜덤한 문자열로 작성해도 작동은 하지만 환경변수로 아래처럼 관리하는 것이 가장 좋다. </p>
</blockquote>
<ul>
<li>.env에 키를 저장</li>
<li>application.yml에서 환경변수 받기</li>
<li>Security Config에서 값 주입받아 사용</li>
</ul>
<p>.env는 .gitignore에 적어놓는 파일로 DB 비밀번호, 보안 키처럼 환경마다 다르고 공개하면 안 되는 값을 저장할 때 사용한다. </p>
<h4 id="필수-설정-및-장점">필수 설정 및 장점</h4>
<ul>
<li>HTTPS 설정 필수 (remember me 쿠키 보안)</li>
<li>장기간 로그인 유지를 구현하기 위해 세션 시간을 늘리지 않아도 된다. (세션 시간을 늘리면 서버 부담 높아짐)</li>
</ul>
<p>일반 세션은 짧게 유지하더라도 세션이 만료되면 서버는 그 세션을 제거하고 나중에 사용자가 다시 사이트에 접속하면 브라우저의 Remember Me 쿠키를 검사해 자동 인증하고 새로운 세션을 생성하는 방식으로 자동 로그인 된다. </p>
<blockquote>
<h4 id="http-vs-https">HTTP VS HTTPS</h4>
<p>HTTP와 HTTPS는 비슷하지만 HTTPS는 HTTP 통신에 암호화와 서버 인증을 추가한 방식이라고 생각하면 보다 쉬워진다. 
그래서 중간에서 누가 통신을 보더라도 내용을 알아보기 어렵고 사용자가 접속한 서버가 진짜 해당 사이트인지도 확인할 수 있다.</p>
</blockquote>
<h2 id="9-role-hierarchy권한-계층">9. Role Hierarchy(권한 계층)</h2>
<p><del>(hierarchy = 하이어라키..)</del></p>
<p>스프링 시큐리티에서 role을 설계하다보면 몇 권한에 대해 계층이 있는 경우가 많다. 
이 시스템의 경우에도 admin &gt; user의 계층을 가진다. 
이 role 계층 구조를 스프링 시큐리티한테 알려줌으로써 구현을 단순화할 수 있다. </p>
<pre><code>// 기존 인가
.requestMatchers(&quot;/user&quot;).hasAnyRole(&quot;USER&quot;, &quot;ADMIN&quot;)

// 계층 설정 후 인가
.requestMatchers(&quot;/user&quot;).hasAnyRole(&quot;USER&quot;)</code></pre><p>admin의 권한이 user의 권한을 포함하고 있다는 것을 시큐리티에게 알려 role에 user만 써도 admin의 접근이 가능하도록 만들 수 있다.</p>
<h3 id="1-role-hierarchy-설정">1) Role Hierarchy 설정</h3>
<h4 id="메소드-형식-명시적으로-접두사-작성">메소드 형식: 명시적으로 접두사 작성</h4>
<pre><code>@Bean
public RoleHierarchy roleHierarchy() {
    return RoleHierarchyImpl.withRolePrefix(&quot;ROLE_&quot;)
            .role(&quot;ADMIN&quot;).implies(&quot;USER&quot;)
            .build();
}</code></pre><h4 id="메소드-형식-role_-접두사-기본-고정">메소드 형식: ROLE_ 접두사 기본 고정</h4>
<pre><code>@Bean
public RoleHierarchy roleHierarchy() {
    return RoleHierarchyImpl.withDefaultRolePrefix()
            .role(&quot;ADMIN&quot;).implies(&quot;USER&quot;)
            .build();
}</code></pre><h4 id="텍스트-형식">텍스트 형식</h4>
<pre><code>@Bean
public RoleHierarchy roleHierarchy() {
    return RoleHierarchyImpl.fromHierarchy(&quot;&quot;&quot;
            ROLE_ADMIN &gt; ROLE_USER
            &quot;&quot;&quot;);
}</code></pre><blockquote>
<h4 id="각-형식의-차이점은-무엇일까">각 형식의 차이점은 무엇일까?</h4>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/fb266295-0771-456d-b005-59cf9dd95e8c/image.png" alt=""></p>
</blockquote>
<p>여기서는 명시적으로 접두사를 작성하는 메소드 형식을 사용하여 권한 포함 정보를 설정했다.</p>
<p>implies를 사용해서 admin이 user의 권한을 포함하도록 한다.</p>
<h2 id="10-access-인가-커스텀">10. access() 인가 커스텀</h2>
<p>실무 환경에서는 단순 ROLE 이외에도 여러 요소에 의해 인가가 결정되기도 한다. </p>
<p>SecurityConfig</p>
<pre><code>// 인가 필터에 대한 설정(admin만 수정)
.requestMatchers(&quot;/admin&quot;).access(customAuthorizationManager())</code></pre><p>SecurityConfig</p>
<pre><code>private AuthorizationManager&lt;RequestAuthorizationContext&gt; customAuthorizationManager() {

        return (authentication, context) -&gt; {

            boolean allowed =
                    authentication.get().getAuthorities().stream()
                            .anyMatch(a -&gt; a.getAuthority().equals(&quot;ROLE_ADMIN&quot;));

            return new AuthorizationDecision(allowed);
        };
    }</code></pre><p>/admin 경로에 대한 접근 권한을 hasrole로 하지 않고 커스텀 AuthorizationManager가 판단하도록 설정했다. 해당 매니저는 현재 사용자의 권한 목록을 확인하고 ROLE_ADMIN 권한이 존재하면 접근을 허용하고 그렇지 않으면 거부한다.</p>
<p>여기서는 hasrole 검사와 사실상 별반 다르지 않지만 학습을 위함이고 좀 더 큰 프로젝트에서 도입한다면 유용하게 사용할 수 있다. </p>
<h2 id="11-세션-설정">11. 세션 설정</h2>
<h3 id="1-statefulstateless">1) stateful/stateless</h3>
<p>: SecurityConfig에서 세션 관련 설정을 디폴트로 두면 세션이 state 상태로 설정되며 서버 메모리에 유저 정보가 저장된다. </p>
<p>stateful 상태에서는 서버가 유저를 기억하지만 stateless 상태에서는 로그인을 했지만 다음 HTTP 요청에서는 유저를 인지하지 못하기에 다음 요청마다 사용자가 자신의 인증 정보를 다시 보내야 한다.</p>
<pre><code>// 세션
        http
                .sessionManagement(session -&gt; session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));
</code></pre><p>설정하지 않으면 state 상태</p>
<h4 id="세션-유지-기간state">세션 유지 기간(state)</h4>
<p>서버 세션 유지 기간 설정, 기본 값으로 1800초이고 유지 기간은 마지막 요청 시점으로부터 측정된다. 너무 길게 잡는다면 서버측 메모리에 세션 데이터가 쌓이기 때문에 무리가 갈 수 있다. </p>
<h3 id="2-세션-고정-보호session-fixation-protection">2) 세션 고정 보호(Session Fixation Protection)</h3>
<p>세션 state 방식은 유저를 기억한다.
이는 브라우저에게 JSESSIONID라는 쿠키를 발급하고 브라우저가 서버에 접속하면 JSESSIONID 키 값을 기반으로 서버 메모리에서 유저 정보를 찾는 방식으로 동작한다. </p>
<p>이것에 대해 세션 고정 공격이라는 웹 해킹 기법이 있다. 
<img src="https://velog.velcdn.com/images/j_tnqls311/post/42e6e7b4-9b92-4810-bb5e-38e46c16a47b/image.png" alt=""></p>
<p>해커의 JSESSIONID 값을 일반 유저에게 심고 유저가 로그인을 진행하면 서버 메모리에는 해커의 JSESSIONID 값에 유저의 로그인 정보가 담기게 된다. 이후 해커가 해당 JSESSIONID 값으로 요청을 보내면 유저인척 할 수 있게 된다.</p>
<h3 id="3-세션-고정-공격-방지-방법">3) 세션 고정 공격 방지 방법</h3>
<p>: 로그인이 성공되면 JSESSIONID 값을 변경하도록 설정, 공격자가 로그인 전 세션 ID를 알고 있더라도 로그인 후에는 세션 ID가 달라지므로 인증된 세션을 사용할 수 없다. </p>
<p>세션 ID는 매 요청마다 바뀌는 것이 아니라 로그인 성공 시 변경된다.</p>
<blockquote>
<h4 id="csrf와-세션-고정-공격의-차이">CSRF와 세션 고정 공격의 차이</h4>
<p><strong>CSRF</strong>는 피해자가 이미 정상적으로 로그인한 세션을 공격자가 몰래 이용하는 공격 -&gt; 공격자가 세션 ID를 알 필요 없이, 피해자의 브라우저가 세션 쿠키를 자동으로 보내는 것을 이용
<strong>세션 고정 공격</strong>은 공격자의 세션 ID를 피해자에게 사용하게 만든 뒤, 피해자가 로그인하면 그 세션을 같이 사용하는 공격 -&gt; 피해자가 로그인한 뒤 공격자가 같은 세션 ID를 사용해서 계정을 탈취하는 방식</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 부트3]]></title>
            <link>https://velog.io/@j_tnqls311/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B83</link>
            <guid>https://velog.io/@j_tnqls311/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B83</guid>
            <pubDate>Wed, 15 Jul 2026 16:44:42 GMT</pubDate>
            <description><![CDATA[<h2 id="1-spring-boot-initialisation">1. Spring Boot initialisation</h2>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/39c90f1a-ef07-4832-9a4f-85ce0c3dd0fa/image.png" alt=""></p>
<blockquote>
<h3 id="문제점-1-build-안보임">문제점 1. Build 안보임</h3>
<p>강사님의 화면에선 왼쪽 하단에 Build 망치 아이콘이 떴었는데 내 화면에서는 뜨지 않았다.
따라서 나는 오른쪽 코끼리 아이콘을 눌러 Gradle을 따로 설치해주었다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/03738c36-9b09-40a0-b920-0291c535a430/image.png" alt=""></p>
</blockquote>
<blockquote>
<h3 id="문제점-2-spring-boot-설치-이상">문제점 2. spring boot 설치 이상</h3>
<p>gradle 망치 아이콘과 파일 기본 구조가 강사님의 화면과 달라서 이상함을 느끼긴 했었는데 어떻게 내가 바꾸니까 비슷하게 되길래 괜찮은줄 알았는데 아니었다...
애초에 처음 압축을 풀고 open을 할 때 잘못 넣은 것이었다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/8937338d-917a-4e4a-9ffa-11503701f758/image.png" alt="">
바깥쪽 파일이 아닌 gradle이 설치된 바로 전 파일을 열어야 할 것 같다.</p>
</blockquote>
<blockquote>
<h3 id="gradle">Gradle?</h3>
<p>프로젝트를 빌드하고 필요한 라이브러리를 관리해 주는 도구.
인텔리제이는 개발 환경이고, Gradle은 그 안에서 프로젝트 실행에 필요한 작업을 담당한다.</p>
</blockquote>
<h2 id="2-패키지별-용도">2. 패키지별 용도</h2>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/9056c173-d0e7-4124-a127-d20799e06a58/image.png" alt=""></p>
<h4 id="bulidgradle">bulid.Gradle</h4>
<p>: Gradle에게 프로젝트를 어떻게 관리할지 알려주는 설정 파일(스프링 부트 버전, 자바 버전, 의존성)</p>
<h4 id="mainjava스프링-프로젝트용">main.java(스프링 프로젝트용)</h4>
<p>: 자바 클래스를 자바 코드로만 쓸 수 있는 패키지</p>
<h4 id="mainresources스프링-프로젝트용">main.resources(스프링 프로젝트용)</h4>
<p>: 자바 이외의 정적 자료, html css, 이미지 등 자바의 외적인 것을 저장하는 폴더</p>
<h4 id="test테스트-코드용">test(테스트 코드용)</h4>
<p>: java와 resources를 테스트 볼 수 있는 코드</p>
<h2 id="3-스프링부트-아키텍처">3. 스프링부트 아키텍처</h2>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/366629f0-a59c-4329-8adb-f8ad411d1d38/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/51bbb255-7d24-4a60-bf1f-4cbe6f083400/image.png" alt="">
각 Controller, Service와 같은 패키지로 이동할 때는 DTO라는 데이터를 이동시키는 클래스를 사용하여야 한다. 
그러나 Repository에서 DB로 데이터를 이동시킬 때는 Entity를 사용해야 한다.
-&gt; Entity 필드명이 DB의 테이블과 하나씩 대응되기 때문</p>
<blockquote>
<h4 id="왜-이렇게-단을-나누어야-하나">왜 이렇게 단을 나누어야 하나?</h4>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/7eae16a1-e32e-48d0-a517-14c63f867115/image.png" alt="">
위처럼 하는 일을 명확히 구분하고, 코드의 재사용성을 높이기 위해서</p>
</blockquote>
<h2 id="4-패키지-작성방법">4. 패키지 작성방법</h2>
<p><strong>DDD(도메인 주도 설계)</strong>: 비슷한 일을 하는 domain(영역)끼리 패키지로 묶고 그 안에 Repository, Service, Controller 등 클래스를 만든다.</p>
<p>ex) <img src="https://velog.velcdn.com/images/j_tnqls311/post/684de7a3-72bc-4de4-8896-582d7338a129/image.png" alt=""></p>
<blockquote>
<h4 id="왜-controller-패키지는-domain-안에-넣지-않고-뺴둘까">왜 Controller 패키지는 domain 안에 넣지 않고 뺴둘까?</h4>
<p>개수가 적고 가장 먼저 사용자에게 요청을 받을 것이기 때문에 관습적으로 앞으로 빼둔다.</p>
</blockquote>
<h3 id="1-config">1) Config</h3>
<p>: 프로젝트 전체에 걸쳐 특정한 설정을 할당해줄때 사용하는 패키지(DB 설정, Spring Security 등)</p>
<p>다만 자바 클래스 작성은 매우 번거롭기 때문에 스프링 부트에서는 간단하게 변수 설정으로 해당 기능을 지원하고 있다.</p>
<p>변수 설정 경로: main -&gt; src -&gt; resources -&gt; applicatinon.properties</p>
<blockquote>
<h4 id="config와-applicationproperties">config와 application.properties</h4>
<p><strong>config</strong> -  이 객체를 스프링 빈으로 등록해라, 보안 요청은 이런 규칙으로 처리해라, CORS는 이런 경로에서 허용해라 
-&gt; <em>스프링이 어떻게 동작할지 코드로 정의</em>
<strong>application.properties</strong> - DB 주소는 어디인지, 서버 포트는 몇 번인지, JPA를 어떤 방식으로 동작시킬지
-&gt; <em>환경에 따라 바뀔 수 있는 설정값을 저장</em></p>
</blockquote>
<h3 id="2-applicationproperties">2) application.properties</h3>
<h4 id="mysql-db-연결">MySQL DB 연결</h4>
<pre><code>// RDB 중 연결할 DB 엔진에 대한 드라이브 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
// DB가 구동되는 IP (주소)
spring.datasource.url=jdbc:mysql://아이피:포트/디비명?useSSL=false&amp;useUnicode=true&amp;serverTimezone=Asia/Seoul&amp;allowPublicKeyRetrieval=true
// 접속 아이디/비밀번호
spring.datasource.username=root
spring.datasource.password=root</code></pre><h4 id="jpa-설정">JPA 설정</h4>
<pre><code>// 자바 클래스 기반으로 MySQL 테이블 자동 생성 여부 (update : 스프링 기반으로 MySQL 테이블 업데이트)
spring.jpa.hibernate.ddl-auto=update</code></pre><p>엔티티를 기반으로 MtSQL 테이블이 자동으로 생성(create) 또는 화면처럼 업데이트(update) 될 수 있도록 할 수 있다. 자리에 create, none, update 등 무엇을 넣느냐에 따라 달라질 수 있다.
기본적으로 개발 환경에서는 update, 배포 환경에서는 none을 사용한다.</p>
<h4 id="포트설정">포트설정</h4>
<pre><code>// 스프링 Entity와 MySQL 테이블 필드간 필드명 작명 전략
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl</code></pre><h3 id="3-util">3) util</h3>
<p>: 전체적인 스프링 프로젝트에서 사용될 메소드들을 클래스로 만들어서 저장해두는 패키지</p>
<h2 id="5-security-추가">5. Security 추가</h2>
<h3 id="1-인증">1) 인증</h3>
<p>: 호텔 프론트에서 특정사람에게 키를 부여</p>
<h3 id="2-인가">2) 인가</h3>
<p>: 호텔 각각의 서비스(방, 라운지, 커뮤니티)에서 접근하는 사람을 검증 -&gt; 인증이 되었다고 하더라도 특정 인가가 필요한 서비스에는 접근하지 못할수도 있음</p>
<p>ex) 게시글 작성 프로그램에서도 로그인 후 글을 쓰는 것은 가능하지만(인증만 필요한 서비스) 글을 삭제하거나 수정하기 위해서는 인가가 필요하다.(글의 작성자 또는 관리인)</p>
<p>-&gt; 이러한 인증/인가를 처음부터 구현하는 것은 어렵기 때문에 &#39;spring security&#39;라는 의존성을 활용</p>
<blockquote>
<h4 id="의존성-추가하는-방법">의존성 추가하는 방법</h4>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/51f52d3c-0648-44e8-bf23-8d479f506de8/image.png" alt="">
<img src="https://velog.velcdn.com/images/j_tnqls311/post/7fe83e67-7803-461f-b669-bcd6d4d17fbd/image.png" alt="">
스프링 이니셜라이저 -&gt; add -&gt; 의존성 추가 -&gt; explore -&gt; dependencies에 starter와 starter-test 복사 -&gt; 인텔리제이 build.gradle dependencies 쪽에 붙여넣기 -&gt; 코끼리</p>
</blockquote>
<h2 id="6-프로젝트에-시큐리티-설정">6. 프로젝트에 시큐리티 설정</h2>
<p>application.properties로도 설정이 가능하지만 앞서 말한 차이점 때문에 config 클래스 작성 필요</p>
<pre><code>@Configuration
@EnableWebSecurity
public class Securityconfig {


    //스프링 시큐리티에서는 비밀번호를 암호화해서 저장해야 함. 이때 사용할 암호화 클래스
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    // 시큐리티 설정
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        // csrf 보안 해제
        http
                .csrf(csrf -&gt; csrf.disable());

        // 접근 경로별 인가 설정
        http
                .authorizeHttpRequests(auth -&gt; auth
                        .requestMatchers(&quot;/**&quot;).permitAll());

        // 로그인 방식 설정 Form 로그인 방식
        http
                .formLogin(Customizer.withDefaults());

        return http.build();
    }
}</code></pre><h3 id="1-어노테이션">1) 어노테이션</h3>
<p>@Configuration - 이 클래스가 Spring 설정 클래스라는 뜻
@EnableWebSecurity - 스프링 시큐리티의 웹 보안 설정 활성화, 요청이 들어보면 security filter를 거쳐 인증 인가를 검사하도록 한다.
@Bean(BCryptPasssordEncoder) - BCryptPasssordEncoder 객체를 Spring Bean으로 등록하는 코드
회원가입 시 비밀번호를 그대로 DB에 저장하면 안되기 때문에 암호화 작업을 거친다.</p>
<h3 id="2-접근-경로별-인가-설정">2) 접근 경로별 인가 설정</h3>
<p>permitAll()로 현재 모든 경로에 로그인 없이 접근할 수 있다. </p>
<blockquote>
<h4 id="csrf란">csrf란?</h4>
<p>: 로그인한 사용자를 속여서 원하지 않는 요청을 보내게 만드는 공격
따라서 spring security는 CSRF Token를 발급하고 Session Cookie와  CSRF Token 모두 맞아야 요청을 처리한다. CSRF 토큰이 없으면 403 Forbidden을 반환하기 때문에 이 프로젝트에서는 기능을 사용하지 않는다.</p>
</blockquote>
<blockquote>
<h4 id="form-로그인-방식">Form 로그인 방식</h4>
<p>: Spring Security가 기본으로 제공하는 (세션)로그인 방식(로그인 화면도 Spring이 만들어 주고, 로그인 처리도 Spring이 해준다)</p>
</blockquote>
<h2 id="7-user-도메인-작성">7. User 도메인 작성</h2>
<p>블로그 서비스에서 요구사항</p>
<ul>
<li>유저별 게시글 CRUD</li>
<li>유저별 인증/인가 (로그인)</li>
</ul>
<p>Create: 회원가입
Read: 로그인 / 회원정보 읽기
Update: 회원정보 수정 / 회원탈퇴
Delete: 회원탈퇴</p>
<h3 id="1-테이블-생성---entity">1) 테이블 생성 - Entity</h3>
<pre><code>@Getter
@Setter
@Entity
public class UserEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    private String password;

    private String nickname;
}</code></pre><p>@Id + @GeneratedValue - PK 번호 + 할당 전략
private &#39;데이터타입&#39; &#39;변수명&#39; - 데이터베이스 필드와 일대일 매핑</p>
<p>(@Setter는 실무인 경우, 쓰는 것을 지양하는 것이 좋지만 간단한 구현을 위해 사용하겠다.)</p>
<blockquote>
<h4 id="setter를-잘-안쓰는-이유">@Setter를 잘 안쓰는 이유</h4>
<p>: 객체의 값을 아무 곳에서나 변경할 수 있어서 객체의 일관성이 깨질 수 있기 때문</p>
</blockquote>
<h3 id="2-user-계급화">2) User 계급화</h3>
<blockquote>
<h4 id="수직적-계급-vs-수평적-계급">수직적 계급 vs 수평적 계급</h4>
<p><strong>수직적 계급</strong>: 여러 유저를 class 별로 관리하기 위함 -&gt; 메소드에 접근하는 유저의 role 체크를 통해 구현(시큐리티 설정)
<strong>수평적 계급</strong>: 동일한 계급 내부에서 각각의 유저를 관리하기 위함(같은 user 계급이더라도 자신의 정보와 자신의 글만 삭제 가능) -&gt; 메소드에 접근하려는 username과 현재 로그인한 유저의 username을 비교(직접 구현)</p>
</blockquote>
<p>아래는 UserRoleType(enum) 코드내용이다.</p>
<pre><code>@Getter
public enum UserRoleType {

    ADMIN(&quot;아드민&quot;),
    USER(&quot;유저&quot;);

    private final String discription;

    UserRoleType(String discription) {
        this.discription = discription;
    }
}</code></pre><p>미리 정해진 값만을 사용할 수 있는 enum의 자료형에 따라 enum의 상수, 즉 사용자 권한을 admin과 user로 제한했다.</p>
<p>아래는 UserRoleType 클래스가 추가됨에 따라 추가된 UserEntity 코드이다.</p>
<pre><code>@Enumerated(EnumType.STRING)
    private UserRoleType role;</code></pre><p>엔티티의 UserRoleType 필드에 enum 값을 저장하지만 DB에는 문자열로 저장하겠다.
-&gt; DB에 저장되는 값 = ADMIN, USER
-&gt; description 필드의 값 = &#39;아드민&#39;, &#39;유저&#39;</p>
<blockquote>
<h4 id="왜-enum값을-문자열로-저장할까">왜 enum값을 문자열로 저장할까?</h4>
<p>@Enumerated은 순서 번호로 데이터를 저장한다.
이때 값이 추가되거나 순서가 바뀌면 문제가 생길 수 있으므로 문자열로 전환하여 저장한다.
steps할 때 리스트로 받고 문자열로 저장했다가 다시 리스트로 반환하도록 프로그램을 잘못만든 경험이 있어 이것도 프로젝트를 간단하게 만들기 위한 임시적인 건가 했는데 아니엇당</p>
</blockquote>
<blockquote>
<h4 id="role과-enum">Role과 enum</h4>
<p><strong>Role</strong> - 인증/인가를 쉽게 구현하도록 도와주는 &#39;스프링 시큐리티&#39;는 role이라는 속성을 가지고 수직적인 계급을 관리한다.
<strong>enum</strong> - role의 경우 String 형태의 필드보다는 미리 계급을 정해서 enum화 시켜주는 것이 좋다.</p>
</blockquote>
<blockquote>
<h4 id="role-계층-추가">Role 계층 추가</h4>
<p>서비스에 대해 수직적 계급을 만들었지만 스프링 시큐리티 프레임워크는 우리가 만든 enum 계급을 알지 못함
-&gt; SecurityConfig 에 @Bean 메소드를 등록하여 우리의 계급 체계를 알려주어야 한다.
(admin이 user보다 권한이 넓다는 것을 인식시키는 것)</p>
</blockquote>
<p>아래는 추가된 SercurityConfig 코드내용이다.</p>
<pre><code>    // 시큐리티 role 수직적 계층 적용
    @Bean
    public RoleHierarchy roleHierarchy() {

        return RoleHierarchyImpl.withRolePrefix(&quot;ROLE_&quot;)
                .role(UserRoleType.ADMIN.toString()).implies(UserRoleType.USER.toString())
                .build();
    }</code></pre><h4 id="시큐리티-role-수직적-계층-적용">시큐리티 role 수직적 계층 적용</h4>
<p>admin 권한을 가진 사람은 user 권한도 포함해서 가진 것으로 설정
.role(UserRoleType.ADMIN.toString()): 이 권한이
.implies(UserRoleType.USER.toSpring()): 이 권한을 포함한다.</p>
<h3 id="3-테이블에-행위를-수행할-메소드-작성---service">3) 테이블에 행위를 수행할 메소드 작성 - Service</h3>
<p>정보 저장소에 대한 CRUD</p>
<p>repository에서 값을 받아와 요청을 처리해야 한다.(private)
필드변수 선언 + 값 초기화 </p>
<pre><code>@Service
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;


    public UserService(UserRepository userRepository, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.userRepository = userRepository;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }
}</code></pre><h4 id="1-create">(1) Create</h4>
<p>: UserEntity에 새로운 회원 등록 = 회원가입</p>
<pre><code>// 유저 한명 생성
    @Transactional
    public void createOneUser(UserRequestDTO dto){
        String username = dto.getUsername();
        String password = dto.getPassword();
        String nickname = dto.getNickname();

        // 동일한 username이 있는지 확인
        if (userRepository.existsByUsername(username)){
            return;
        }

        // 유저에 대한 Entity 생성: DTO -&gt; Entity 및 추가정보 set
        UserEntity entity = new UserEntity();
        entity.setUsername(username);
        entity.setPassword(bCryptPasswordEncoder.encode(password));
        entity.setNickname(nickname);
        entity.setRole(UserRoleType.USER);

        // Entity 저장
        userRepository.save(entity);</code></pre><blockquote>
<h4 id="transactional">@Transactional</h4>
<p>Serivce 코드를 작성할 때 유용하게 쓰이는 어노테이션
코드가 잘못 실행되거나 오류가 나서 되돌아가게 될 경우 기존 작업이 소실되지 않도록 해준다. </p>
</blockquote>
<h4 id="2-read">(2) Read</h4>
<p>UserEntity에 존재하는 <strong>특정</strong> 유저 읽기
회원 정보를 응답할 DTO</p>
<pre><code>// 유저 한명 읽기
    @Transactional(readOnly = true)
    public UserResponseDTO readOneUser(String username){

        UserEntity entity = userRepository.findByUsername(username).orElseThrow();

        UserResponseDTO dto = new UserResponseDTO();
        dto.setUsername(entity.getUsername());
        dto.setRole(entity.getRole().toString());

        return dto;
    }
}</code></pre><blockquote>
<h4 id="orelsethrow">orElseThrow</h4>
<p>user가 존재하지 않으면 Optional이 빈 상태로 반환될 수 있기 때문에 orElseThrow를 사용하여 값이 없을 경우 예외를 발생시킨다.</p>
</blockquote>
<p>UserEntity에 존재하는 <strong>모든</strong> 유저 읽기</p>
<p>회원 정보를 응답할 DTO</p>
<pre><code>// 유저 모두 읽기
    @Transactional(readOnly = true)
    public List&lt;UserResponseDTO&gt; readAllUsers(){
        List&lt;UserEntity&gt; list = userRepository.findAll();

        List&lt;UserResponseDTO&gt; dtos = new ArrayList&lt;&gt;();
        for (UserEntity user : list){
            UserResponseDTO dto = new UserResponseDTO();
            dto.setUsername(user.getUsername());
            dto.setNickname(user.getRole().toString());

            dtos.add(dto);
        }

        return dtos;</code></pre><p>로그인용 read</p>
<p>로그인은 HTML Form 태그로 클라이언트가 보낸 username/password를 Service 계층에서 username 기반으로 유저를 조회하여 비교 검증하는 방식이다. 하지만 스프링 시큐리티를 사용하면 비교검증 파트는 자동으로 생성되기 때문에 userEntity 테이블에서 특정 username 데이터를 읽는 메소드만 구현하면 된다. </p>
<pre><code>@Service
public class UserService implements UserDetailsService {</code></pre><pre><code>// 유저 로그인 (로그인 같은 경우 읽기지만 시큐리티 형식으로 맞춰야 함)
    @Transactional(readOnly = true)
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        UserEntity entity = userRepository.findByUsername(username).orElseThrow();

        return User.builder()
                .username(entity.getUsername())
                .password(entity.getPassword())
                .roles(entity.getRole().toString())
                .build();
    }</code></pre><p>여기서 사용한 User는 직접 만든 UserEntity가 아니라 Spring Security에서 제공하는 org.springframework.security.core.userdetails.User 클래스이다.</p>
<h4 id="3-update">(3) Update</h4>
<p>UserEntity에 이미 존재하는 특정 유저의 정보 수정(닉네임, 비밀번호)</p>
<ol>
<li>사용자가 수정할 username과 데이터 -&gt; DTO로 보냄</li>
<li>Service에서 username 기반으로 데이터를 read</li>
<li>DTO에서 받은 데이터를 기반으로 기존 데이터에 신규 값을 초기화</li>
<li>저장</li>
</ol>
<pre><code>// 유저 한명 수정
    @Transactional
    public void updateOneUser(UserRequestDTO dto, String username){

        //  기존 유저 정보 읽기
        UserEntity entity = userRepository.findByUsername(username).orElseThrow();

        // 비밀번호가 입력된 경우에만 변경
        if (dto.getPassword() != null &amp;&amp; !dto.getPassword().isEmpty()){
            entity.setPassword(bCryptPasswordEncoder.encode(dto.getPassword()));
        }

        // 닉네임이 입력된 경우에만 변경
        if (dto.getNickname() != null &amp;&amp; !dto.getNickname().isEmpty()){
            entity.setNickname(dto.getNickname());
        }

        // 새 정보 저장
        userRepository.save(entity);
    }</code></pre><blockquote>
<p>stpes 할때에는 프로필카드 수정기능을 만들때 모든 정보를 삭제하고 다시 작성해서 넣도록 했는데 이렇게 비교해서 값이 입력된 것만 수정되도록 하는것이 더 일반적인 방법인 것 같다. </p>
</blockquote>
<h4 id="4-delete">(4) Delete</h4>
<p>service</p>
<pre><code>// 유저 정보 삭제
    @Transactional
    public void deleteOneUser(String username ){
        userRepository.deleteByUsername(username);
    }</code></pre><p>repository</p>
<pre><code>@Transactional
    void deleteByUsername(String username);</code></pre><p>User CRUD 접근 제한</p>
<pre><code>// 유저 접근 권한 체크
    public Boolean isAccess(String username) {

        // 현재 로그인 되어 있는 유저의 username
        String sessionUsername = SecurityContextHolder.getContext().getAuthentication().getName();
        // 현재 로그인 되어 있는 유저의 role
        String sessionRole = SecurityContextHolder.getContext().getAuthentication().getAuthorities().iterator().next().getAuthority();

        // 수직적으로 ADMIN이면 무조건 접근 가능
        if (&quot;ROLE_ADMIN&quot;.equals(sessionRole)) {
            return true;
        }

        // 수평적으로 특정 행위를 수행할 username이 세션(현재 로그인한) username과 같은지
        if (username.equals(sessionUsername)) {
            return true;
        }

        // 나머지 불가
        return false;
    }</code></pre><h3 id="4-user-도메인-메소드와-다른-도메인-메소드를-조합한-api-작성---controller">4) User 도메인 메소드와 다른 도메인 메소드를 조합한 API 작성 - Controller</h3>
<p>사용자가 경로에 데이터를 보냈을 때 올바른 메서드가 실행되도록 Controller에 엔드포인트를 만들어야 한다. </p>
<h4 id="api-엔드포인트">API 엔드포인트</h4>
<ul>
<li><p>회원가입 페이지: GET&quot;/user/join&quot;</p>
</li>
<li><p>회원가입 진행: POST&quot;/user/join&quot;</p>
</li>
<li><p>회원 수정 페이지: GET&quot;/user/update/{username}&quot;</p>
</li>
<li><p>회원 수정 진행: POST&quot;/user/update{username}&quot;</p>
</li>
<li><p>회원 삭제 진행: DELETE&quot;/user/delete/{username}&quot;</p>
</li>
<li><p>로그인 페이지: GET&quot;/login&quot;(스프링 시큐리티 기본제공)</p>
</li>
<li><p>로그인 진행: POST&quot;/login&quot;(스프링 시큐리티 기본제공)</p>
</li>
</ul>
<p>회원가입 controller 엔드포인트 설정</p>
<pre><code>@Controller
public class UserController {

    private final UserService userService;

    public UserController(UserService userService){
        this.userService = userService;
    }

    // 회원가입: 페이지 응답
    @GetMapping(&quot;/join&quot;)
    public String joinPage() {
        return &quot;join&quot;;
    }

    // 회원가입: 수행
    @PostMapping(&quot;/join&quot;)
    public String joinProcess(UserRequestDTO dto) {
        userService.createOneUser(dto);
        return &quot;redirect:/join&quot;;
    }
}</code></pre><p>회원 수정 controller 엔드포인트 설정</p>
<pre><code>// 회원 수정 : 페이지 응답
    @GetMapping(&quot;/user/update/{username}&quot;)
    public String updatePage(@PathVariable(&quot;username&quot;) String username, Model model) {

        // 본인 또는 ADMIN 권한만 접근 가능
        if (userService.isAccess(username)) {
            UserResponseDTO dto = userService.readOneUser(username);
            model.addAttribute(&quot;USER&quot;, dto);
            return &quot;update&quot;;
        }

        // 접근 권한이 없으면 로그인 페이지로 반환
        return &quot;redirect:/login&quot;;
    }

    // 회원 수정 : 수행
    @PostMapping(&quot;/user/update/{username}&quot;)
    public String updateProcess(@PathVariable String username, UserRequestDTO dto) {

        // 본인 또는 ADMIN 권한만 접근 가능
        if (userService.isAccess(username)) {
            userService.updateOneUser(dto, username);
        }

        // 수정 완료된 후 다시 회원 수정 페이지
        return &quot;redirect:/user/update/&quot; + username;
    }</code></pre><p>URL의 {username} 부분에는 수정할 회원의 아이디가 들어간다.
@PathVariable은 URL 경로에 포함된 값을 메서드의 매개변수로 받아오는 역할을 한다.</p>
<h2 id="8-board-도메인-작성">8. Board 도메인 작성</h2>
<h3 id="1-테이블-생성-entity">1) 테이블 생성 Entity</h3>
<h4 id="boardentity">BoardEntity</h4>
<pre><code>@Entity
@Getter
@Setter
public class BoardEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    @Column(columnDefinition = &quot;TEXT&quot;)
    private String content;
}</code></pre><p>@Id를 사용하여 Id를 PK로 지정하고 @GeneratedValue로 이를 자동생성하게 한다.
content의 경우 VARCHAR로 받으면 받을 수 있는 글의 한도가 너무 적기 때문에 TEXT를 사용하여 받는다. </p>
<h3 id="2-user와-join">2) User와 JOIN</h3>
<p>User와 Board 간의 상관관계를 만든다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/ce8ffc12-4a76-485a-b742-3ffa1bf0c024/image.png" alt=""></p>
<p>User 하나당 여러개의 게시글을 가질 수 있고 게시글 하나당 한명의 유저와 연결된다. -&gt; <strong>일대다관계</strong></p>
<h4 id="boardentity-추가코드">BoardEntity 추가코드</h4>
<pre><code>@ManyToOne
    private UserEntity userEntity;</code></pre><p>여러 개의 게시글이 하나의 사용자와 연결될 수 있으므로 BoardEntity에는 @ManyToOne을 사용한다.
BoardEntity 테이블에 사용자 PK를 참조하는 외래 키가 생성된다.</p>
<h4 id="userentity-추가코드">UserEntity 추가코드</h4>
<pre><code>@OneToMany(mappedBy = &quot;userEntity&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
    private List&lt;BoardEntity&gt; boardEntityList = new ArrayList&lt;&gt;();

    // 유저에 대해 새로운 글을 추가할 때 : 추가할 글을 받아서 연관관계에 매핑해줌
    public void addBoardEntity(BoardEntity entity) {
        entity.setUserEntity(this);
        boardEntityList.add(entity);
    }

    // 유저에 대해 기존 글을 삭제할 때 : 삭제할 글을 받아서 연관관계에서 뺌
    public void removeBoardEntity(BoardEntity entity) {
        entity.setUserEntity(null);
        boardEntityList.remove(entity);
    }</code></pre><p>한 명의 사용자가 여러 개의 게시글을 가질 수 있으므로 UserEntity에는 @OneToMany를 사용한다.
게시글이 여러 개 존재할 수 있기 때문에 List&lt; BoardEntity &gt; 형태로 관리한다.</p>
<p>cascade = CascadeType.ALL
사용자 엔티티에 수행한 영속성 작업을 해당 사용자의 게시글에도 함께 적용 -&gt; 사용자를 삭제하면 연결된 게시글도 함께 삭제될 수 있다.</p>
<p>CascadeType.ALL에는 저장, 수정, 삭제 등의 전파 기능이 모두 포함된다.</p>
<p>addBoardEntity()와 removeBoardEntity()는 연관관계 양쪽의 값을 함께 변경하여 객체 상태를 일관되게 유지하기 위한 연관관계 편의 메서드이다.</p>
<h3 id="3-테이블에-행위를-수행할-메소드-작성-service">3) 테이블에 행위를 수행할 메소드 작성 Service</h3>
<p>CRUD 작성 </p>
<h4 id="1-create-1">(1) Create</h4>
<p>게시글 생성 DTO</p>
<pre><code>@Getter
@Setter
public class BoardRequestDTO {

    private String title;
    private String content;
}</code></pre><p>Service</p>
<pre><code>// 게시글 생성
    @Transactional
    public void createOneBoard(BoardRequestDTO dto) {

        // 게시글 dto -&gt; entity
        BoardEntity boardEntity = new BoardEntity();
        boardEntity.setTitle(dto.getTitle());
        boardEntity.setContent(dto.getContent());

        // entity 저장
        boardRepository.save(boardEntity);

        // 현재 게시글을 작성하는 유저
        String username = SecurityContextHolder.getContext().getAuthentication().getName();

        // 해당 유저의 Entity 가져오기
        UserEntity userEntity = userRepository.findByUsername(username).orElseThrow();

        // 연관 관계 만드는 메소드 호출
        userEntity.addBoardEntity(boardEntity);
        userRepository.save(userEntity);
    }</code></pre><p>게시글 작성 후 등록과 동시에 UserEntity와 연관관계가 생긴다. </p>
<h4 id="2-read-1">(2) Read</h4>
<p><strong>BoardEntity에 존재하는 특정 글 읽기</strong>
게시글 읽기 DTO</p>
<pre><code>@Getter
@Setter
public class BoardResponseDTO {

    private Long id;
    private String title;
    private String content;
}</code></pre><p>Service</p>
<pre><code>// 게시글 하나 읽기
    @Transactional(readOnly = true)
    public BoardResponseDTO readOneBoard(Long id) {

        BoardEntity boardEntity = boardRepository.findById(id).orElseThrow();

        BoardResponseDTO dto = new BoardResponseDTO();
        dto.setId(boardEntity.getId());
        dto.setTitle(boardEntity.getTitle());
        dto.setContent(boardEntity.getContent());

        return dto;
    }</code></pre><p><strong>BoardEntity에 존재하는 모든 게시글 읽기</strong></p>
<pre><code>// 게시글 모두 읽기
    @Transactional(readOnly = true)
    public List&lt;BoardResponseDTO&gt; readAllBoards() {

        List&lt;BoardEntity&gt; list = boardRepository.findAll();

        List&lt;BoardResponseDTO&gt; dtos = new ArrayList&lt;&gt;();
        for (BoardEntity boardEntity : list) {
            BoardResponseDTO dto = new BoardResponseDTO();
            dto.setId(boardEntity.getId());
            dto.setTitle(boardEntity.getTitle());
            dto.setContent(boardEntity.getContent());

            dtos.add(dto);
        }

        return dtos;
    }</code></pre><p>게시글은 모두가 읽을 수 있도록 설정한다. </p>
<h4 id="3-update-1">(3) Update</h4>
<p>BoardEntity에 존재하는 특정 게시글 수정
-&gt; 특정 게시글 id를 기반으로 데이터 수정 진행</p>
<pre><code>// 게시글 하나 수정
    @Transactional
    public void updateOneBoard(Long id, BoardRequestDTO dto) {

        // 기존의 id에 대한 게시글 데이터 불러오기
        BoardEntity boardEntity = boardRepository.findById(id).orElseThrow();

        // 게시글 dto -&gt; entity
        boardEntity.setTitle(dto.getTitle());
        boardEntity.setContent(dto.getContent());

        // 수정된 데이터 저장
        boardRepository.save(boardEntity);
    }</code></pre><p>게시글 수정 및 삭제의 경우 작성한 유저만 수정, 삭제를 할 수 있어야하기 때문에 접근 권한 확인이 필요</p>
<p>Service 코드 추가</p>
<pre><code>// 유저 접근 권한 체크
    public Boolean isAccess(Long id) {

        // 현재 로그인 되어 있는 유저의 username
        String sessionUsername = SecurityContextHolder.getContext().getAuthentication().getName();
        // 현재 로그인 되어 있는 유저의 role
        String sessionRole = SecurityContextHolder.getContext().getAuthentication().getAuthorities().iterator().next().getAuthority();

        // 수직적으로 ADMIN이면 무조건 접근 가능
        if (&quot;ROLE_ADMIN&quot;.equals(sessionRole)) {
            return true;
        }

        // 특정 게시글 id에 대해 본인이 작성 했는지 확인
        String boardUsername = boardRepository.findById(id).orElseThrow().getUserEntity().getUsername();
        if (sessionUsername.equals(boardUsername)) {
            return true;
        }

        // 나머지 불가
        return false;
    }</code></pre><h4 id="4-delete-1">(4) Delete</h4>
<p>BoardEntity에 존재하는 특정 게시글 삭제
-&gt; 특정 id를 기반으로 게시글 데이터를 삭제하는 방법</p>
<pre><code>// 게시글 하나 삭제
@Transactional
public void deleteOneBoard(Long id) {

    boardRepository.deleteById(id);
}</code></pre><h3 id="4-board-도메인-메소드와-다른-도메인-메소드를--조합한-api-작성-controller">4) Board 도메인 메소드와 다른 도메인 메소드를  조합한 API 작성 Controller</h3>
<ul>
<li>글 작성 페이지 : GET“/board/create”</li>
<li>글 작성 진행 : POST“/board/create”</li>
<li>글 목록 페이지 : GET“/board/read”</li>
<li>글 읽기 페이지 : GET“/board/read/{id}”</li>
<li>글 수정 페이지 : GET“/board/update/{id}”</li>
<li>글 수정 진행 : POST“/board/update/{id}”</li>
<li>글 삭제 진행 : POST“/board/delete/{id}”</li>
</ul>
<pre><code>@Controller
public class BoardController {

    private final BoardService boardService;

    public BoardController(BoardService boardService) {
        this.boardService = boardService;
    }

    // 글 생성 : 페이지 응답
    @GetMapping(&quot;/board/create&quot;)
    public String createPage() {

        return &quot;createBoard&quot;;
    }

    // 글 생성 : 수행
    @PostMapping(&quot;/board/create&quot;)
    public String createProcess(BoardRequestDTO dto) {

        boardService.createOneBoard(dto);

        // 글 수정 완료 후 수정된 글목록 페이지로 이동
        return &quot;redirect:/board/read&quot;;
    }</code></pre><p>redirect:은 수정사항을 반영하여 페이지를 보여준다는 뜻</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[3. Steps 코드작성]]></title>
            <link>https://velog.io/@j_tnqls311/3.-Steps-%EC%BD%94%EB%93%9C%EC%9E%91%EC%84%B1</link>
            <guid>https://velog.io/@j_tnqls311/3.-Steps-%EC%BD%94%EB%93%9C%EC%9E%91%EC%84%B1</guid>
            <pubDate>Thu, 09 Jul 2026 16:22:47 GMT</pubDate>
            <description><![CDATA[<h1 id="3-프로필-카드-수정">3. 프로필 카드 수정</h1>
<h2 id="1-dto">1) DTO</h2>
<p>Request 1개(ProfileCardUpdaterequest), certificates와 ProfileCard Response 2개(CertificatesResponse / ProfileCardResponse)</p>
<h2 id="2-controller">2) controller</h2>
<pre><code>package com.likelion.step.profilecard.controller;


import com.likelion.step.profilecard.dto.ProfileCardResponse;
import com.likelion.step.profilecard.dto.ProfileCardUpdateRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/api/profile&quot;)

public class ProfileCardController {

    private final ProfileCardService profileCardService;

    @PatchMapping
    public ProfileCardResponse updateProfileCard(@RequestBody ProfileCardUpdateRequest request) {

        Long userId = 1L; // 유저 기능 구현 뒤 수정

        return profileCardService.updateProfileCard(userId, request);
    }
}
</code></pre><h3 id="1-어노테이션">(1) 어노테이션</h3>
<h4 id="restcontroller">@Restcontroller</h4>
<p>이 클래스가 REST API를 처리하는 컨트롤러라는 뜻
망약 수정 요청이 들어오면 여기있는 메서드가 실행된다.
@Controller가 html 페이지를 반환하는 용도로 많이 쓴다면 @RestController는 Json을 반환하는 API 용도라고 할 수 있다.</p>
<h4 id="requiredargsconstructor">@RequiredArgsConstructor</h4>
<p>final이 붙은 변수만 생성자로 만들어주는 lombok 어노테이션</p>
<h4 id="requestmapping">@RequestMapping</h4>
<p>이 controller의 기본 주소를 알려주는 어노테이션</p>
<h4 id="patchmapping">@PatchMapping</h4>
<p>HPPT PATCH 요청(일부 수정)을 처리하는 메서드라는 뜻</p>
<h4 id="requestbody">@RequestBody</h4>
<p>요청으로 들어온 Json을 자배 객체로 바꿔서 받아주는 어노테이션
-&gt; 요청 본문(JSON)을 ProfileCardUpdateRequest 객체로 변환해서 request라는 변수에 담아줘</p>
<h2 id="3-entity">3) entity</h2>
<p>ProfileCard / Certificates</p>
<h3 id="1-profilecard">(1) ProfileCard</h3>
<pre><code>package com.likelion.step.profilecard.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor
@Table(name = &quot;profile_Card&quot;)
public class ProfileCard {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long profileCardId;

    private String collaborationTags;

    private String selfIntroduce;

    private Long userId;

    public ProfileCard(String collaborationTags, String selfIntroduce, Long userId) {
        this.collaborationTags = collaborationTags;
        this.selfIntroduce = selfIntroduce;
        this.userId = userId;
    }

    public void updateProfileCard(String collaborationTags, String selfIntroduce) {
        this.collaborationTags = collaborationTags;
        this.selfIntroduce = selfIntroduce;
    }

}</code></pre><h4 id="id">@Id</h4>
<p>기본키(PK)</p>
<h4 id="generatedvaluestratgy--generationtypeidentity">@GeneratedValue(stratgy = GenerationType.IDENTITY)</h4>
<p>기본키 자동생성 어노테이션
괄호 속은 자동생성 방식 중 하나 -&gt; DB의 AUTO_INCREMENT 기능을 사용해서 번호를 증가시켜라</p>
<h4 id="updateprofilecard">updateProfileCard()</h4>
<p>이미 존재하는 객체를 수정하는 메서드
void - 값을 반환하지 않음(저장, 수정, 삭제)
String, int, ProfielResponse - 작업 후 결과를 돌려줌</p>
<h3 id="2-certificates">(2) Certificates</h3>
<pre><code>package com.likelion.step.profilecard.entity;

import lombok.NoArgsConstructor;
import lombok.Getter;
import jakarta.persistence.*;

@Entity
@Getter
@NoArgsConstructor
@Table(name =&quot;certificates&quot;)
public class Certificates {

    @Id
    @GeneratedValue
    private Long certificatesId;

    private String certificates;

    private Long profileCardId;

    public Certificates(String certificates, Long profileCardId) {
        this.certificates = certificates;
        this.profileCardId = profileCardId;
    }
}
</code></pre><h2 id="4-repository">4) repository</h2>
<h3 id="1profilecardrepository">(1)ProfileCardRepository</h3>
<pre><code>package com.likelion.step.profilecard.repository;

import com.likelion.step.profilecard.entity.ProfileCard;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface ProfileRepository extends JpaRepository&lt;ProfileCard, Long&gt; {

    Optional&lt;ProfileCard&gt; findByUserId(Long userId);


}</code></pre><h4 id="extends-jparepositoryprofilecard-long">extends JpaRepository&lt;ProfileCard, Long&gt;</h4>
<p>JpaRepository&lt;엔티티, PK자료형&gt;
-&gt; ProfileCard 엔티티 관리하는 레포고 기본키 타입은 Long이다.</p>
<blockquote>
<h4 id="lombok-vs-jparepository">lombok vs JpaRepository</h4>
<p>둘다 개발자가 써야할 코드를 자동으로 만들어준다는 공통점이 있지만
lombok - 생성자, Getter, Setter 등 (자바 코드 자동 생성)
Spring Data JPA - SQL과 Repository 메서드 (SQL 자동 생성)</p>
</blockquote>
<h4 id="optional-profilecard--findbyuseridlong-userid">Optional&lt; ProfileCard &gt; findByUserId(Long userId)</h4>
<p>find -&gt; 조회한다.
ByUserId -&gt; userID 컬럼을 통해</p>
<p>Optinal - null을 직접 반환하지 않아 오류를 방지
없는 사용자일 경우 오류가 발생하는 것을 막을 수 있음</p>
<h3 id="2-certificatesrepository">(2) CertificatesRepository</h3>
<pre><code>package com.likelion.step.profilecard.repository;

import com.likelion.step.profilecard.entity.Certificates;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CertificatesRepsitory extends JpaRepository&lt;Certificates, Long&gt; {

    void deleteByProfileCardId(Long profileCardId);

}</code></pre><h4 id="void-deletebyprofilecardidlong-profilecardid">void deleteByProfileCardId(Long profileCardId);</h4>
<p>삭제 기능 담당
따라서 find가 아닌 delete를 사용</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2. Steps 코드작성]]></title>
            <link>https://velog.io/@j_tnqls311/2.-Steps-%EC%BD%94%EB%93%9C%EC%9E%91%EC%84%B1</link>
            <guid>https://velog.io/@j_tnqls311/2.-Steps-%EC%BD%94%EB%93%9C%EC%9E%91%EC%84%B1</guid>
            <pubDate>Thu, 09 Jul 2026 13:58:26 GMT</pubDate>
            <description><![CDATA[<h1 id="2-프로필-카드-조회-api">2. 프로필 카드 조회 API</h1>
<h2 id="1-feat1-가져오기">1) feat/#1 가져오기</h2>
<p>프로필 카드 조회 API를 만들기에 앞서 프로필 카드 생성 API를 만들 때 사용했던 Profile 엔티티와 Repository를 사용하기 위해 terminal에서 merge하여 정보를 가져온다.</p>
<h2 id="2-조회-responsedto만들기">2) 조회 responseDTO만들기</h2>
<p>엔티티가 certificates와 profileCard 두개로 나뉘어짐에 따라 DTO 또한 두개로 나누어 만들었다.</p>
<h3 id="1어노테이션">(1)어노테이션</h3>
<h4 id="allargsconstructor">@AllArgsConstructor</h4>
<blockquote>
<h4 id="allargsconstructor-vs-noargsconstructor">AllArgsConstructor vs NoArgsConstructor</h4>
<p>NoArgsConstructor는 아무 값도 받지 않는 생성자를 만들어 값을 입력받는다.
-&gt; 프로필 카드 생성 시 생성자를 사용하여 빈 객체를 만들고 값을 입력받는다.
AllArgsConstructor는 모든 필드를 매개변수로 받는 생성자를 만든다.
-&gt; 조회나 응답 DTO는 값을 직접 채워 반환히기 때문에 AllArgsConstructor</p>
</blockquote>
<h3 id="2">(2)</h3>
<p><code>private CertificatesResponse certificates;</code>
ProfileCardGetResponse 안에 CertificatesResponse 타입의 필드 하나를 넣겠다.</p>
<p>Json으로 보면 </p>
<pre><code>&quot;certificates&quot;: {
  &quot;recentThree&quot;: [&quot;컴퓨터 활용 능력 1급&quot;, &quot;ADsP&quot;],
  &quot;othersCount&quot;: 0
}</code></pre><p>certificates 안에 여러 값이 들어가기 때문에 String이나 List가 아닌 새로운 DTO 객체로 만든 것</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1. Steps 코드작성]]></title>
            <link>https://velog.io/@j_tnqls311/Steps-%EC%BD%94%EB%93%9C%EC%9E%91%EC%84%B1</link>
            <guid>https://velog.io/@j_tnqls311/Steps-%EC%BD%94%EB%93%9C%EC%9E%91%EC%84%B1</guid>
            <pubDate>Tue, 07 Jul 2026 20:27:52 GMT</pubDate>
            <description><![CDATA[<h1 id="1-프로필카드-작성-api">1. 프로필카드 작성 API</h1>
<h2 id="1-패키지-만들기">1) 패키지 만들기</h2>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/d9a1a1c0-10d9-464d-ac62-92065b748cdb/image.png" alt="">
java 파일 밑 step에 controller / dto / entity / repository / service 패키지를 만들었다. </p>
<p>각 패키지는 다음의 역할을 수행한다. </p>
<h4 id="controller--사용자의-요청을-가장-먼저-받는-계층">controller:  사용자의 요청을 가장 먼저 받는 계층</h4>
<p>사용자가 프로필 카드를 생성하면</p>
<pre><code>POST /profile-cards</code></pre><p>라는 요청이 Controller로 들어온다. controller는 이 요청을 받은 후 필요한 데이터를 service로 전달하고 처리 결과를 다시 사용자에게 반환하는 역할을 수행한다.
-&gt; 사용자와 서비스를 연결</p>
<h4 id="service-실제-기능을-수행하는-계층">service: 실제 기능을 수행하는 계층</h4>
<p>ex) 프로필 카드 생성 / 프로필 카드 조회 / 입력값 검증
-&gt; 프로그램의 핵심 기능 수행</p>
<h4 id="repository-데이터베이스와-상호작용하는-계층">repository: 데이터베이스와 상호작용하는 계층</h4>
<p>JPA를 이용하여 저장 / 조회 / 수정 / 삭제 와 같은 작업을 한다.
-&gt; 데이터베이스와 연결</p>
<blockquote>
<h4 id="jpa란">JPA란?</h4>
<p>JPA는 자바 객체와 데이터베이스를 연결해 주는 기술입니다. 개발자가 SQL을 직접 작성하지 않아도 JPA가 SQL을 자동으로 생성하여 데이터베이스와 통신할 수 있도록 도와줍니다.</p>
</blockquote>
<h4 id="entity-데이터베이의-테이블을-자바-객체로-표현하는-계층">entity: 데이터베이의 테이블을 자바 객체로 표현하는 계층</h4>
<p>ERD에 있는 profileCard 엔티티를 직접 profileCard 클래스로 표현한다.
각 속성은 </p>
<pre><code>private Long profileCardID;
private String introduce;
private String tag;</code></pre><p>와 같이 변수로서 작용한다.
-&gt; ERD 테이블과 매핑</p>
<h4 id="dto-계층-간-데이터를-전달하기-위한-계층">dto: 계층 간 데이터를 전달하기 위한 계층**</h4>
<p>Data Transfer Object
프로필 카드 생성 요청이 들어오면 데이터를 DTO에 담어 service로 전달한다.
entity를 직접 사용하는 것보다 필요한 데이터만 전달할 수 있어서 보안과 유지보수 측면에서 유리하다.
-&gt; 계층 간 데이터를 전달하는 객체</p>
<blockquote>
<h4 id="왜-dto를-사용하는지에-대한-추가적인-설명">왜 dto를 사용하는지에 대한 추가적인 설명</h4>
<p>사용자가 프로필 카드를 생성하려고 할 때 보내는 데이터는 tag와 introduce이다.
하지만 entity를 그대로 사용하면 tag와 introduce 뿐만 아니라 profileCardID, userID 모두 받을 수 있게 된다. 따라서 불필요한 데이터의 노출을 막을 수 있고 역할을 분리하여 유지보수가 보다 쉬워진다.</p>
</blockquote>
<h2 id="2-entity-패키지-작성">2) Entity 패키지 작성</h2>
<h3 id="1어노테이션">(1)어노테이션</h3>
<pre><code>package com.likelion.step.profilecard.entity;

import jakarta.persistence.*;

@Entity
@Table(name = &quot;profileCard&quot;)
public class profileCard {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long profileCardID;

    private String tag;

    private String introduce;

    private Long userID;

}</code></pre><h4 id="entity-이-클래스가-데이터베이스erd의-테이블엔티티이라는-것을-jpa에게-알림">@Entity: 이 클래스가 데이터베이스(ERD)의 테이블(엔티티)이라는 것을 JPA에게 알림</h4>
<h4 id="tablename--profilecard-테이블-이름을-지정">@Table(name = &quot;profileCard&quot;): 테이블 이름을 지정</h4>
<p>데이터베이스의 어떤 테이블과 연결하도록 명시하는 역할</p>
<h4 id="id-기본키pk라는-뜻">@Id: 기본키(PK)라는 뜻</h4>
<p>현재 <code>profileCardID</code> 를 기본키로 설정한 상태</p>
<h4 id="generatedvaluestrategy--genertivetypeientity">@GeneratedValue(strategy = GenertiveType.IENTITY)</h4>
<p>: 데이터베이스가 자동으로 번호를 증가시켜주라는 뜻</p>
<h4 id="getter-lombok이-자동으로-getter-매서드를-만들어-주는-것">@Getter: Lombok이 자동으로 getter 매서드를 만들어 주는 것</h4>
<p>private으로 보호되는 정보를 가져올 수 있는 매서드</p>
<blockquote>
<h4 id="그렇다면-왜-public을-쓰지-않고-private과-getter를-쓸까">그렇다면 왜 public을 쓰지 않고 private과 Getter를 쓸까?</h4>
<p>public을 사용했을 시 어디서든 값을 마음대로 바꿀 수 있다. 하지만 private을 썼을시 그 클래스에서만 값을 다룰 수 있기 때문에 데이터가 보호된다.
-&gt; <strong>캡슐화</strong> - 외부에서 함부로 접근하거나 수정하지 못하도록 보호하는 것</p>
</blockquote>
<h4 id="noargsconstructor-lombok이-기본-생성자를-자동으로-만들어-주는-것">@NoArgsconstructor: Lombok이 기본 생성자를 자동으로 만들어 주는 것</h4>
<blockquote>
<h4 id="lombok이-뭘까">Lombok이 뭘까?</h4>
<p>Lombok은 반복해서 작성하는 코드를 자동으로 만들어주는 라이브러리이다.</p>
</blockquote>
<h3 id="2문자열">(2)문자열</h3>
<pre><code>private String tag;</code></pre><p>String인 이유 - ERD에서는 ENUM이지만 아직 Enum 클래스를 만들지 않았기 때문에 임시로 String으로 저장</p>
<pre><code>private Long userID;</code></pre><p>원래는 </p>
<pre><code>@ ManyToOne
private User user;</code></pre><p>가 맞지만 아직 User 기능이 구현되지 않았으므로 임시로 회원 ID만 저장</p>
<h3 id="3생성자">(3)생성자</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/8b7b5bc8-5d75-4f92-a9f8-003eec399c3d/image.png" alt=""></p>
<p>객체를 만들때 처음 값을 넣어주는 코드
<code>this</code>는 현재 만들어지고 있는 객체 자기 자신을 의미한다(하나는 필드, 다른 하나는 생성자 매개변수)</p>
<p>ex) <code>this.tag = tag;</code> = 현재 객체의 tag 필드에 생성자로 들어온 tag 값을 넣겠다.
<code>new ProfileCard(&quot;BACKEND&quot;,&quot;안녕하세요&quot;,1L);</code>
생성자 실행
<code>public ProfileCard(String tag, String introduce, Long Id)</code>
따라서 값이 다음과 같이 들어온다.
tag = &quot;BACKEND&quot;
introduce = &quot;안녕하세요&quot;
userId = 1L</p>
<h2 id="3-repository-패키지-작성">3). repository 패키지 작성</h2>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/4866c27b-4855-4d36-91cd-5172c6b719b6/image.png" alt=""></p>
<h3 id="1interface">(1)interface</h3>
<p>왜 class가 아닌 interface를 쓸까?
<code>extends JpaRepository</code> 때문
JpaRepository에는 데이터를 다룰 때 사용하는 기능(CRUD 기능)들이 이미 구현되어 있기 때문에</p>
<pre><code>public interface ProfileCardRepository
extends JpaRepository&lt;ProfileCard, Long&gt;</code></pre><p>만 써도 JPA가 실제로 동작하는 repository를 자동으로 만들어준다.</p>
<h3 id="2">(2)</h3>
<pre><code>JpaRepository&lt;ProfileCard, Long&gt;</code></pre><p>profileCard는 어떤 entity를 관리하는 repository인지를 의미
Long은 PK 타입을 의미</p>
<h2 id="4-dto-패키지-작성">4). dto 패키지 작성</h2>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/a03c4ba9-ac13-446d-ac8f-a848cbfa2865/image.png" alt=""></p>
<h2 id="5-certification-entity--repository--dto수정">5). Certification entity / repository / dto(수정)</h2>
<p>entity: 
<img src="https://velog.velcdn.com/images/j_tnqls311/post/597835af-3de9-4351-87f2-1ac15aad59f3/image.png" alt=""></p>
<p>repository:
<img src="https://velog.velcdn.com/images/j_tnqls311/post/6a837997-3da0-4e27-82f1-0d94b5ce6843/image.png" alt=""></p>
<p>dto:
<img src="https://velog.velcdn.com/images/j_tnqls311/post/c755d373-615e-4c0b-86b7-836102e40cb7/image.png" alt=""></p>
<h2 id="6-service">6). service</h2>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/2f77325a-211b-422c-91b2-c3ebfbbda4c1/image.png" alt=""></p>
<h3 id="1-어노테이션">(1) 어노테이션</h3>
<h4 id="service-이-클래스가-비즈니스-로직을-담당하는-service라고-알려줌">Service: 이 클래스가 비즈니스 로직을 담당하는 service라고 알려줌</h4>
<h4 id="requiredargsconstructor-lombok-자동완성-생성자">RequiredArgsConstructor: Lombok 자동완성 생성자</h4>
<blockquote>
<h4 id="entity에서의-생성자-vs-service에서의-생성자">entity에서의 생성자 vs service에서의 생성자</h4>
<p>entity에서는 객체를 생성하며 데이터를 초기화하기 위해
service(@RequiredArgsConstructor)에서는 repository 같은 의존성을 생성자를 통해 주입받기 위해</p>
</blockquote>
<h3 id="2-1">(2)</h3>
<pre><code>private final ProfileCardRepository profileCardRepository;</code></pre><p>여기서 final은 한번만 초기화되고 이후에는 바뀌지 않음을 의미한다.
(값은 계속 바꿀 수 있지만 처리하는 repository는 처음 연결된 이것만 사용한다는 뜻)</p>
<h3 id="3">(3)</h3>
<pre><code>package com.likelion.step.profilecard.sevice;

import com.likelion.step.profilecard.dto.ProfileCardCreateRequest;
import com.likelion.step.profilecard.entity.Certification;
import com.likelion.step.profilecard.entity.ProfileCard;
import com.likelion.step.profilecard.repository.CertificationRepository;
import com.likelion.step.profilecard.repository.ProfileCardRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor

public class ProfileCardService {

    private final ProfileCardRepository profileCardRepository;
    private final CertificationRepository certificationRepository;

    public Long createProfileCard(ProfileCardCreateRequest request){
        ProfileCard profileCard = new ProfileCard(
                request.getTag(),
                request.getIntroduce(),
                request.getUserId()
        );

        ProfileCard saveProfileCard = profileCardRepository.save(profileCard);

        Certification certification = new Certification(
                request.getCertification(),
                saveProfileCard.getProfileCardId()
        );

        certificationRepository.save(certification);

        return saveProfileCard.getProfileCardId();
    }
}</code></pre><pre><code>public Long createProfileCard(ProfileCardCreateRequest request)</code></pre><p>이 메서드가 프로필 카드 생성 기능이다.
request는 사용자 요청을 DTO에 담아서 service로 넘기는 역할을 수행한다.</p>
<pre><code>ProfileCard profileCard = new ProfileCard(
                request.getTag(),
                request.getIntroduce(),
                request.getUserId()
        );</code></pre><p>이 메서드는 entity를 새로 생성하는 역할을 수행한다.
DTO는 그냥 데이터를 전달하고 담아오는 객체이므로 DB에는 저장할 수 없다.</p>
<pre><code>ProfileCard savedProfileCard =
        profileCardRepository.save(profileCard);</code></pre><p>데이터를 DB에 저장하기 위한 메소드이다. save()를 호출하면 JPA가 SQL을 자동 생성해서 DB에 저장한다.</p>
<pre><code>Certification certification = new Certification(
                request.getCertification(),
                saveProfileCard.getProfileCardId()
        );

        certificationRepository.save(certification);</code></pre><p>새로운 자격증 객체를 만든다. 어떤 프로필 카드의 자격증인지 연결하기 위해 savedProfileCard.getProfileCardId()를 사용한다. (FK로서 profileCardId를 사용하기 위해)</p>
<h2 id="7-controller">7) controller</h2>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/bf69768f-4bda-4859-9d1d-2b3f1ca1e21c/image.png" alt=""></p>
<h2 id="8-postman-테스트">8) Postman 테스트</h2>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/7704f045-3e0a-47b7-925c-596fa5df02a0/image.png" alt=""></p>
<blockquote>
<h4 id="포트번호-불일치">포트번호 불일치</h4>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[협업 GitHub 사용법]]></title>
            <link>https://velog.io/@j_tnqls311/%ED%98%91%EC%97%85-GitHub-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@j_tnqls311/%ED%98%91%EC%97%85-GitHub-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Tue, 30 Jun 2026 05:56:45 GMT</pubDate>
            <description><![CDATA[<h2 id="1-브랜치-사용">1. 브랜치 사용</h2>
<h3 id="1-브랜치-만드는-명령어">1) 브랜치 만드는 명령어</h3>
<pre><code>git checkout -b 브랜치 이름</code></pre><p>브랜치 생성</p>
<pre><code>git push</code></pre><p>만든 브랜치 올리기</p>
<h3 id="2-master-브랜치">2) master 브랜치</h3>
<p>실제 유저들이 사용하는 코드, 최대한 완벽한 코드만 올린다.</p>
<blockquote>
<p>master 브랜치 보호하기
함부로 코드를 수정하고 push 할 수 없도록 보호해야 함</p>
</blockquote>
<ul>
<li>protect this branch 클릭 </li>
<li>Lock branch(코드 Push 할 수 없음) 체크</li>
<li>Require a request before merging(모든 사람에게 Request 창을 띄워 모두에게 동의를 받으면 코드가 올라가는 시스템) 체크</li>
</ul>
<h3 id="3-develope-브랜치">3) develope 브랜치</h3>
<p>개발자들이 연습장처럼 사용하는 브랜치, 다른 사람의 코드와 충돌이 일어나는지 확인하고 수정하는 과정을 여기서 거침</p>
<h2 id="2-project-만들기">2. Project 만들기</h2>
<h3 id="1-project-생성">1) Project 생성</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/62db26ef-375a-4774-9d54-eebbf0751137/image.png" alt="">
상단 Project 클릭해서 페이지 이동</p>
<h3 id="2-new-project-선택">2) New project 선택</h3>
<h3 id="3-board-양식-사용">3) Board 양식 사용</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/4694e455-907d-41a1-9ca6-4ecfa071237d/image.png" alt="">
우측 중간에 있는 View 버튼을 통해 스타일을 변경할 수 있음.</p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/4a18c841-9759-4948-966b-09cd99942e5c/image.png" alt="">
Todo에서 만들어야 할 기능 Issue들을 정리해 역할을 분담할 수 있다.</p>
<h3 id="4--feature-브랜치-만들기">4)  feature 브랜치 만들기</h3>
<p>나 혼자쓰는 연습장이라고 할 수 있음 
내가 맡은 기능을 이 feature 브랜치에서 구현한 다음  develope  브랜치로 올려 충돌 등 문제가 생기지 않으면 master 브랜치로 올리는 구조</p>
<p><strong>이 feature 브랜치는 꼭 develope 브랜치에서 따와서 만들어야 함.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[기초 GitHub 사용법]]></title>
            <link>https://velog.io/@j_tnqls311/%EA%B8%B0%EC%B4%88-GitHub-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@j_tnqls311/%EA%B8%B0%EC%B4%88-GitHub-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Mon, 29 Jun 2026 16:16:26 GMT</pubDate>
            <description><![CDATA[<h2 id="1-repository-만들기">1. repository 만들기</h2>
<h4 id="1-new-클릭">1) New 클릭</h4>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/30a980f0-d033-4ebb-a3d4-7cd036c46451/image.png" alt=""></p>
<h4 id="2-repository-이름과-공개범위-설정하기">2) repository 이름과 공개범위 설정하기</h4>
<p>주로 Public으로 해야 나중에 어디 보냈을 때 다른 사람들이 볼 수 있다고 함
<img src="https://velog.velcdn.com/images/j_tnqls311/post/7f8a452a-9623-4bff-84f9-ea751c659446/image.png" alt=""></p>
<h4 id="3-하단-create-repository-클릭">3) 하단 Create Repository 클릭</h4>
<h2 id="2-git-설치하기">2. Git 설치하기</h2>
<h4 id="1-git-설치-후-git-bash-실행">1) Git 설치 후 Git bash 실행</h4>
<h4 id="2-git-config---global-username-영어이름">2) git config --global user.name &quot;영어이름&quot;</h4>
<h4 id="3-git-config---global-useremail-ace10433gmailcom">3) git config --global user.email &quot;<a href="mailto:ace10433@gmail.com">ace10433@gmail.com</a>&quot;</h4>
<h4 id="4-git-config---list">4) git config --list</h4>
<p>잘 저장되었는지 확인
<img src="https://velog.velcdn.com/images/j_tnqls311/post/3b374876-8b81-4c03-a54e-6da8afda4b3f/image.png" alt=""></p>
<h2 id="3-인텔리제이에서-terminal-하기">3. 인텔리제이에서 terminal 하기</h2>
<h4 id="1-view---tool-windows---terminal">1) View -&gt; Tool Windows -&gt; Terminal</h4>
<p>Alt + F12 로도 가능</p>
<h4 id="2-git-설치">2) git 설치</h4>
<pre><code>git init</code></pre><h4 id="3-코드-업로드-범위설정">3) 코드 업로드 범위설정</h4>
<pre><code>git add .</code></pre><p>.은 전부 올린다는 뜻, 만약 일부만 올리고 싶다면 이름 써주면 됌
띄어쓰기 주의</p>
<h4 id="4-업로드-확인">4) 업로드 확인</h4>
<pre><code>git status</code></pre><p><img src="https://velog.velcdn.com/images/j_tnqls311/post/827e1808-b322-4ab7-b7de-27b099e5cdfb/image.png" alt="">
그냥 내가 올리려고 했던 파일이 잘 올라갔는지 확인하는 용도</p>
<h2 id="3-push">3. push</h2>
<h4 id="1-first-commit">1) First commit</h4>
<pre><code>git commit -m &quot;first commit&quot;</code></pre><h4 id="2-github-경로설정">2) GitHub 경로설정</h4>
<pre><code>git remote add origin https://github.com/tnqlsdl123/practice1.git</code></pre><blockquote>
<h4 id="주의">주의</h4>
<p>SHH 방식의 주소를 쓰면 안되고 HTTP 주소를 써야함</p>
</blockquote>
<h4 id="3-push-1">3) Push</h4>
<pre><code>git push -u origin master</code></pre><p>밑에 뜨는 코드 중 <code>master -&gt; master</code> 이런 메세지가 떠있으면 성공</p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/44028cc0-5da7-4f0c-89dd-bf1b3edcc4a0/image.png" alt=""></p>
<h2 id="4-인텔리제이에-코드를-추가했을-때">4. 인텔리제이에 코드를 추가했을 때</h2>
<h4 id="1-추가하여-올리는-법">1) 추가하여 올리는 법</h4>
<pre><code>git add .</code></pre><p>앞에서 init을 해두었기 때문에 하지않아도 됌.</p>
<h4 id="2-히스토리-설정하기">2) 히스토리 설정하기</h4>
<pre><code>git commit -m &quot;second commit&quot;</code></pre><h4 id="3-github에-올리기">3) GitHub에 올리기</h4>
<pre><code>git pust origin master</code></pre><h4 id="4-새로고침">4) 새로고침</h4>
<p>-&gt; 전 버전에 비해 어떤 부분 코드가 바뀌었는지 알 수 있어서 좋음.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[<멋쟁이사자처럼> 7번째 벨로그]]></title>
            <link>https://velog.io/@j_tnqls311/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-7%EB%B2%88%EC%A7%B8-%EB%B2%A8%EB%A1%9C%EA%B7%B8</link>
            <guid>https://velog.io/@j_tnqls311/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-7%EB%B2%88%EC%A7%B8-%EB%B2%A8%EB%A1%9C%EA%B7%B8</guid>
            <pubDate>Sat, 23 May 2026 07:14:50 GMT</pubDate>
            <description><![CDATA[<h1 id="1-aws-가입하기">1. AWS 가입하기</h1>
<p>허허.....
생각해보니 나 해외결제 카드가 없었구나...........
뒤늦게 해외결제 카드를 신청했지만 주말에 신청해서 다음주중에나 받아볼 수 있을 듯 하다..</p>
<p>그래서 일단 부모님 카드로 해보려고 했는데ㅜ 그랬는데..</p>
<blockquote>
<h3 id="문제점-1">문제점 1.</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/370401da-b277-4c05-bf7f-3c252b78d198/image.png" alt="">
카드 비밀번호를 입력하다 어쩌다 3회 이상 틀리게 되었는데(근데 틀렸을 리가 없는데) 그때부터 비밀번호 입력하는 창으로도 안넘어가고 이 창만 뜬다..
제미나이한테 물어봤더니 창 다닫고 시크릿모드 켜고 AWS 다시 들어가서 해보라고 해서 해보았는데 그래도 똑같은 창이 뜨고 다른 카드도 써봤는데(해외결제가 되는지 잘 모르겠는..) 똑같은 오류창이 뜬다 
비밀번호 오류  너무 많이 내서 이런건지 그러면 어떡해야 하는지 모르겠어서 물어봤더니 오류 창 보면 그게 아니고 정보가 잘못된 거 같다고 하고 그런데 잘못된 정보가 없는데ㅜㅜ</p>
</blockquote>
<h1 id="2-aws-ec2-배포-해보기">2. AWS EC2 배포 해보기</h1>
<h2 id="1-배포란">1. 배포란?</h2>
<p>: 다른 사용자들이 인터넷을 통해서 내가 만든 웹 페이지에 접속할 수 있도록 하는 것</p>
<p>-&gt; 현재 우리가 만든 게시판 페이지는 localhost로 만들어져 있기 때문에 다른 사람들이 접근이 불가능한 주소이다. 하지만 배포를 하고 나면 다른 사람들도 내가 만든 게시판 페이지에 접속이 가능하게 된다. </p>
<h2 id="2-ec2란">2. EC2란?</h2>
<p>: Elastic Computer Cloud, AWS에서 빌려 쓰는 가상 컴퓨터</p>
<p>-&gt; 내가 만든 게시판 페이지는 localhost:8080으로만 접속이 가능하다. 하지만 EC2에 올리면 인터넷 주소를 통해 다른 사람들도 접속이 가능하게 된다.</p>
<p>백엔드 서버를 배포할 때 주로 사용하고 프론트엔드 서버를 배포할 때는 주로 다른 방법을 사용한다고 한다.</p>
<h2 id="3-ec2-기본-설정">3. EC2 기본 설정</h2>
<h3 id="1-region-설정하기">1) region 설정하기</h3>
<p><strong>region이란?</strong>
: 인프라를 지리적으로 나누어 배포한 각각의 데이터 센터를 의미</p>
<p>쉽게 말해 EC2를 통해 빌려서 쓸 수 있는 컴퓨터들이 전 세계적으러 다양하게 분포해 있는데, 이 컴퓨터들이 위치한 위치들을 region이라고 한다.</p>
<p>EC2를 실행시키고 제일 먼저 설정하게 되는 것이 region인데 그렇다면 어떤 기준으로 선택하는 것이 좋을까?
-&gt; 웹의 주된 사용자들의 위치와 가장 가까운 region을 선택하는 것이 좋다.</p>
<p>지리적으로 멀면 멀수록 속도가 느려진다.</p>
<p>내가 한국 사람이니까 region 아시아 태평양 서울로 설정해야지! (X)
내가 한국 사람일지라도 나는 도쿄에 있는 사람들이 주로 사용하는 웹을 만들 거니까 아시아 태평양 도쿄로 region을 설정해야지! (O)</p>
<h3 id="2-기본-설정">2) 기본 설정</h3>
<h4 id="1-이름-및-태그">1. 이름 및 태그</h4>
<h4 id="2-os-선택하기">2. OS 선택하기</h4>
<p>: 어떤 OS를 가진 컴퓨터를 사용할 것인가?
-&gt; <strong>Ubuntu</strong></p>
<blockquote>
<h4 id="왜-ubuntu를-사용해야-하는가">왜 Ubuntu를 사용해야 하는가?</h4>
<p>Windows나 MacOS 같은 경우 컴퓨터를 모르는 사람들도 쉽게 쓸 수 있도록 하기 위해 편한 기능들을 너무 많이 넣어 놓음
-&gt; 서버의 성능과 속도가 느려짐</p>
</blockquote>
<h4 id="3-인스턴스">3. 인스턴스</h4>
<p>: EC2에서 빌리는 컴퓨터 1대를 의미]
<strong>인스턴스 유형</strong>: 컴퓨터 사양을 의미
-&gt; t2.micro: 프리 티어가 사용가능한 인스턴스 유형
(딱히 안좋다거나 하지 않음.)</p>
<h4 id="4-키-페어로그인">4. 키 페어(로그인)</h4>
<p>: EC2 컴푸터에 아무나 접근하지 못하도록 거는 암호
-&gt; 키 페어 이름은 직관적으로 알아볼 수 있게 설정, 이름 이외 나머지는 설정할 필요 X</p>
<p>이 파일을 통해 EC2에 접근할 수 있으므로 잃어버리거나 유출되지 않도록 <strong>잘 보관</strong>해 두어야 함!!!</p>
<h3 id="3-보안그룹-설정">3) 보안그룹 설정</h3>
<h4 id="1-네트워크-설정">1. 네트워크 설정</h4>
<p>네트워크 설정 우측 상단 &#39;편집&#39; 클릭</p>
<h4 id="2-보안그룹">2. 보안그룹</h4>
<p>: AWS 클라우드에서의 네트워크 보안을 의미
IF. 인터넷에서 일부 사용자가 EC2 인스턴스에 접근하려고 한다.
-&gt; EC2 인스턴스 주위의 보안 그룹을 만들고 보안그룹에 규칙이 지정되어 있으면 트래픽의 출입을 설정할 수 있다.</p>
<p>인바운드 트래픽: 외부에서 EC2 인스턴스로 들어오는 트래픽
아웃바운드 트래픽: 내부에서 EC2 인스턴스 바깥으로 나가는 트래픽</p>
<blockquote>
<h4 id="어떻게-보안그룹을-설정해야-하는가">어떻게 보안그룹을 설정해야 하는가?</h4>
</blockquote>
<ul>
<li>보안 그룹 생성 선택</li>
<li>보안 그룹 이름 설정</li>
<li>인바운드 보안 그룹 규칙</li>
<li>보안 그룹 지정 추가
  사용자 지정 TCP -&gt; HTTP
  포트 80 / 사용자 위치 미정</li>
</ul>
<blockquote>
<h4 id="ip와-port">IP와 Port</h4>
<p>IP: 네트워크 상에서 특정 컴퓨터를 가리키는 주소
Port: 한 컴퓨터 내에서 실행되고 있는 특정 프로그램의 주소
ex) localhost:8080에서 8080이 포트 번호이다. </p>
</blockquote>
<blockquote>
<h4 id="브라우저-창에-포트번호를-입력하지-않는-이유는-무엇일까">브라우저 창에 포트번호를 입력하지 않는 이유는 무엇일까?</h4>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/615c486a-ecc3-40f4-9c61-ada99e341520/image.png" alt="">
기본적으로 80번 포트로 통신을 보내게 설정되어 있다.
포트를 명시한다면 기본 80번 포트가 아니라 다른 곳으로 갈 수도 있다.</p>
</blockquote>
<blockquote>
<h4 id="잘-알려진-포트">잘 알려진 포트</h4>
<p>포트 번호는 0<del>65,535번까지 사용할 수 있다.
그 중에서도 0</del>1,023번 까지의 포트 번호는 주요 통신을 위한 암묵적 룰에 의해 이미 정해져 있다.
이렇게 이미 역할이 정해져 있는 포트 번호를 &#39;잘 알려진 포트&#39;라고 한다.</p>
</blockquote>
<ul>
<li>22번(SSH, Secure Shell Protocol): 원격 접속을 위한 포트 번호</li>
<li>80번(HTTP): HTTP로 통신할 때 사용</li>
<li>443번(HTTPS): HTTPS로 통신할 때 사용</li>
</ul>
<h3 id="4-스토리지-구성-설정">4) 스토리지 구성 설정</h3>
<p>: EC2도 컴퓨터처럼 저장공간이 필요함 = EBS(Elastic Block Storage)
EBS와 같은 저장 공간을 조금 더 포괄적인 용어로 &#39;스토리지&#39;라고 한다.</p>
<ul>
<li>스토리지 크기: 프론 티어에서 무료로 제공하는 최대 기가바이트는 30</li>
<li>볼륨 유형: 제일 최근의 것. gp3 사용</li>
</ul>
<p>우측 하단 인스턴스 시작</p>
<h2 id="4-ec2-접속하기">4. EC2 접속하기</h2>
<ul>
<li>퍼블릭 IPv4 주소: 빌린 컴퓨터의 IP주소</li>
<li>인스턴스 상태: 인스턴스 중지 / 시작 / 재부팅 / 최대 절전 모드 / 종료(<strong>삭제</strong>)</li>
</ul>
<p>모니터링
실제 서버를 운영할 때 자주 보게되는 창으로 </p>
<p>우측 상단 &#39;연결&#39; 클릭
인스턴스에 연결
이름 ubuntu로 설정 후 연결 클릭
EC2 인스턴스 서버 들어감</p>
<h2 id="5-탄력적-ip-연결하기">5. 탄력적 IP 연결하기</h2>
<blockquote>
<h4 id="왜-탄력적-ip가-필요할까">왜 탄력적 IP가 필요할까?</h4>
<p>EC2 인스턴스를 생성하면 IP를 할당받게 된다. 하지만 이렇게 할당받은 IP는 EC2인스턴스를 중지시켰다가 다시 실행시켜보면 바뀌어있다. 따라서 중지시켰다가 다시 실행시켜도 바뀌지 않는 고정 IP를 할당받아야 한다.
-&gt; <strong>탄력적 IP</strong> </p>
</blockquote>
<p>탄력적 IP라 유동적으로 상황에 맞게 변화하는 IP인가 생각했는데 그 반대라서 머쓱했다ㅎ</p>
<p><strong>탄력적 IP 설정하는 법</strong></p>
<ol>
<li>&#39;네트워크 및 보안 그룹&#39;에서 &#39;탄력적 IP&#39; 들어가기</li>
<li>우측 상단 &#39;탄력적 IP 주소 할당&#39; 선택</li>
<li>세부사항 건드리지 말고 할당 클릭</li>
<li>식별이 쉽도록 이름 바꾸어 준 후 작업에서 &#39;탄력적 IP 주소 연결&#39; 선택</li>
<li>인스턴스 선택 후 연결 클릭</li>
</ol>
<blockquote>
<h4 id="ec2-종료하는-법">EC2 종료하는 법</h4>
</blockquote>
<ol>
<li>인스턴스 상태 -&gt; 종료</li>
<li>&#39;네트워크 및 보안&#39; -&gt; &#39;탄력적 IP&#39;</li>
<li>작업 -&gt; &#39;탄력적 IP 주소 릴리스&#39; -&gt; 릴리스</li>
</ol>
<h2 id="7-rds">7. RDS</h2>
<h3 id="1-rds란">1) RDS란?</h3>
<p>:관계형 데이터베이스 서비스</p>
<blockquote>
<h4 id="왜-사용하는가">왜 사용하는가?</h4>
<p>로컬 환경에서 개발할 때에는 로컬 환경에 설치된DB를 연결해서 사용한다.
하지만 서버를 배포하고 나면 서버가 내 컴퓨터에 설치된 DB에 연결할 수가 없다. DB도 외부 인터넷에서 접근할 수 있게 같이 배포해주어야 한다.
-&gt; AWS RDS 빌려서 사용</p>
</blockquote>
<blockquote>
<p>EC2에 DB를 직접 설치해서 쓴다면?
안되는 것은 아닌데 현업에서는 보통 분리해서 사용함</p>
</blockquote>
<ul>
<li>별도의 비용이 나오지 않기 때문에 비용 절감은 good</li>
<li>백엔드 서버에 문제 발생 시 DB도 같이 문제가 생길 수 있음</li>
<li>RDS가 제공하는 부가적인 편리한 기능</li>
</ul>
<h3 id="2-rds-생성하기">2) RDS 생성하기</h3>
<ol>
<li>AWS 서비스에서 RDS 검색</li>
<li>데이터베이스 생성(표준 생성)</li>
<li>DB 선택(Maria DB) 탬플릿 선택(프리 티어)</li>
<li>DB 인스턴스 식별자 -&gt; 이름 설정</li>
<li>마스터 암호(데이터 베이스에 접속하기 위한)</li>
<li>퍼블릭 엑세스 &#39;예&#39;</li>
<li>데이터베이스 생성</li>
</ol>
<blockquote>
<h4 id="mysql--maria-db">MySQL / Maria DB</h4>
<p>영상에서는 MySQL로 하는데 MySQL 따라가는 것이 좋을까요 아니면 Maria DB 쓰는게 좋을까요ㅜㅜ 괜히 다르게 했다가 쌤 화면이랑 안맞을까봐 두렵습니다..</p>
</blockquote>
<h3 id="3-보안그룹-세팅">3) 보안그룹 세팅</h3>
<ol>
<li>네트워크 및 보안 -&gt; 보안 그룹 -&gt; 보안 그룹 생성</li>
<li>보안 그룹 이름, 설명 설정(복붙)</li>
<li>인바운드 규칙: 유형에서 DB검색 후 설정</li>
<li>소스 Anywhere-IPv4 설정</li>
<li>보안 그룹 생성</li>
<li>RDS 검색해서 돌아간 후 DB 인스턴스 선택</li>
<li>수정 -&gt; 밑으로 내려서 &#39;연결&#39; 찾기</li>
<li>보안 그룹에서 아까 만들었던 보안 그룹 선택 후 Default 삭제</li>
<li>밑으로 내려서 &#39;계속&#39; 누르기</li>
<li>&#39;즉시 적용&#39; 선택 후 DB 인스턴스 수정</li>
</ol>
<h3 id="4-파라미터-그룹-추가">4) 파라미터 그룹 추가</h3>
<ol>
<li>좌측 파라미터 그룹 클릭</li>
<li>파라미터 그룹 생성 클릭</li>
<li>MySQL 8.0 선택</li>
<li>이름, 설명 설정 -&gt; 생성</li>
<li>방금 만든 파라미터 그룹 들어가서 &#39;수정&#39; 클릭</li>
<li>&#39;character&#39; 검색</li>
<li>character_set_server와 skip-character-set-client-handshake 제외하고 utf8mb4로 설정하기</li>
<li>변경사항 저장 -&gt; 수정</li>
<li>&#39;collation&#39;에서 utf8mb4_unicode_ci로 전부 수정하기</li>
<li>수정 -&gt; &#39;time_zone&#39; 검색</li>
<li>Asia/Seoul 수정</li>
<li>RDS 들어가서 DB 인스턴스 클릭 -&gt; 수정</li>
<li>밑으로 내려가서 &#39;추가 구성&#39; 찾기 -&gt; 아까 만든 파라미터 그룹으로 수정</li>
<li>&#39;즉시적용 설정&#39;</li>
<li>작업에서 재부팅</li>
</ol>
<blockquote>
<h4 id="utf8mb4">utf8mb4?</h4>
<p>데이터베이스에 글자를 저장할 때  사용하는 인코딩(문자 표현) 방식 중 하나
기존에 쓰던 utf4는 이모지를 쓰기에는 바이트가 너무 작게 설정되어 있음
utf8mb4를 사용하면 이모지를 안깨지게 쓸 수 있음</p>
</blockquote>
<blockquote>
<h4 id="utf8mb4_unicode_ci">utf8mb4_unicode_ci</h4>
<p>: 데이터베이스가 문자를 비교하고 정렬할 때 사용하는 기준</p>
</blockquote>
<ul>
<li>utf8mb4 - 이모지까지 저장할 수 있는 Character set</li>
<li>unicode - 문자를 비교할 때 유니코드 표준 규칙을 따르겠다</li>
<li>ci - 대소문자를 구분하기 않겠다. </li>
</ul>
<blockquote>
<h4 id="time_zone">time_zone</h4>
<p>맞추어주지 않으면 글이 작성된 시간, 댓글이 달린 시간 등이 전부 엉뚱하게 기록되기 때문</p>
</blockquote>
<h3 id="5-rds-종료">5) RDS 종료</h3>
<p>작업 -&gt; 삭제</p>
<p>중요!!!</p>
<ul>
<li>최종 스냅샷 생성</li>
<li>자동 백업 보존
반드시 해제하기</li>
</ul>
<h2 id="8-s3와-cloudfront">8. S3와 CloudFront</h2>
<h3 id="1-s3란">1) S3란?</h3>
<p>: AWS에서 제공하는 파일 스토리지 서비스</p>
<blockquote>
<h4 id="왜-사용하는가-1">왜 사용하는가?</h4>
<p>우리가 만드는 웹 서비스에는 많은 파일이 들어감 ex) 이미지, 동영상, 로고...
이러한 파일들을 EC2에 그냥 저장하면 EC2의 용량이 금방 차고 컴퓨터가 느려질 수 있음
-&gt; 글이나 회원 정보같은 가벼운 데이터는 RDS에 저장하고 무거운 이미지와 같은 파일은 S3에 따로 모아서 저장</p>
</blockquote>
<ul>
<li>무제한 용량</li>
<li>안정성 </li>
<li>저렴한 비용</li>
</ul>
<h3 id="2-cloudfront란">2) CloudFront란?</h3>
<p>: 스토리지에 있는 파일을 사용자에게 빠르게 전달해주는 역할</p>
<ul>
<li>컨텐츠 전송 성능을 향상시키기 위해서</li>
<li>HTTPS를 적용하기 위해서</li>
<li>보안적으로 한층 더 강화가 가능</li>
</ul>
<h3 id="3-s3-버킷-생성하기">3) S3 버킷 생성하기</h3>
<ol>
<li>AWS에서 S3 검색 -&gt; 버킷 만들기</li>
<li>이름 설정</li>
<li>&#39;모든 퍼블릭 엑세스 차단&#39; 풀어주기</li>
<li>버킷 만들기</li>
<li>&#39;권한&#39; 페이지 들어가서 권한 정책 편집 선택</li>
<li>&#39;새 문 추가&#39; &#39;S3&#39; -&gt; &#39;GetObjest&#39;</li>
<li>리소스 추가, 유형 Object 선택</li>
<li>리소스 ARN - / 뒤에 * 쓰기(파일 전체 접근) </li>
</ol>
<h1 id="3-배운점-및-느낀점">3. 배운점 및 느낀점</h1>
<p>자꾸 과제를 다음 주차로 미루는 것 같아서 마음이 좋지 않다........ㅜㅜ
진작 신용카드 발급받았으면 이런일이 없었을 거 같은데 미리미리 좀 하는 습관 좀 들이자..
자꾸 과제 미루어서 죄송합니다..ㅜㅜ 앞으로는 이런일 없도록 하겠습니다💧</p>
<p>영어 세글자 줄임말이 왜이렇게 만아요ㅜ 아 헷갈려 RDS 언젠가 스터디 공부할 때 들어본거 같은데.. 기억이 가물가물 ERD랑 헷갈린건가 싶기도 하고.. EC2랑 헷갈린건가 싶기도 하고..</p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/9e4345ab-298e-4b5f-b4b6-e39459f8dcec/image.png" alt="">
제미나이 똑똑하네 utf8mb4가 뭔지 물어보려고 했는데 내 화면을 분석해서 다른 고민까지 해결해줬어..</p>
<p>TMI 이번주 과제를 하기 전에 지인님 벨로그를 우연히 둘러보게 되었는데 지인님 벨로그에 AWS EC2 배포가 있어서 반가웠다!!
지인님도 고생 많이 하신 것이 벨로그에서 느껴져서 신기...
여러분도 지인님 벨로그 구경하세욤ㅎㅎ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[<멋쟁이사자처럼> 6번째 벨로그]]></title>
            <link>https://velog.io/@j_tnqls311/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-6%EB%B2%88%EC%A7%B8-%EB%B2%A8%EB%A1%9C%EA%B7%B8</link>
            <guid>https://velog.io/@j_tnqls311/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-6%EB%B2%88%EC%A7%B8-%EB%B2%A8%EB%A1%9C%EA%B7%B8</guid>
            <pubDate>Mon, 18 May 2026 09:21:30 GMT</pubDate>
            <description><![CDATA[<h1 id="1-들어가기에-앞서">1. 들어가기에 앞서..</h1>
<h2 id="저는-백엔드-개발에-참여하지-않았습니다">저는 백엔드 개발에 참여하지 않았습니다!!!!</h2>
<p>시간도 너무 없었고 든든한 4학년.. 재영님이 계셨기에 그분에게 백의 모든 권한을 일임하고 저는 기획에 힘썼습니다 저는 이번에 거의 기획이었어요🤣 다음 해커톤 때는 백으로서 활약할 수 있도록 더욱 열심히 공부해야겠다는 다짐을 하게되는 계기가 되었습니다~~</p>
<h1 id="2-아이디어-회의">2. 아이디어 회의</h1>
<p>저희는 우선 아이디어가 한번 확정이 되었었고 수익화와 경쟁사 분석, 문제인식까지 해나가던 중 기존 서비스와의 차별점을 찾지 못해 한번 엎은 전적이 있습니다.. 
그때가 아마 저번주 월요일 이맘때쯤이었지요 </p>
<p>7명이서 머리를 맞대고 고민해도 아이디어가 나올 기미가 보이지 않아 막차가 끊길 때쯤 해산하고서도 디스코드에서 만나 회의를 진행했었습니다. </p>
<p>이렇게 되기 전까지는 뭔가 다들 어색하고 의견 내는 것도 조심스러운 느낌이 있었는데 잠을 못자게 하니 있는 아이디어 없는 아이디어 박박 긁어 제안하는 저희가 너무 웃펐었던 기억이 납니다 허허</p>
<p>저도 예상하지 못하게 잠을 잃고 나니 머리가 아득해져서 머리를 정말 쥐어짜냈던 것 같아요 2시경에서야 저희는 기존의 아이디어를 디벨롭해서 진행하는 것으로 주제를 확정지을 수 있었습니다!!!!</p>
<p>그래서 저희가 생각한 서비스는
<strong>&quot;귀갓길 안심 동반자 AI&quot;</strong> 서비스 입니다</p>
<p>혼자 어두운 골목길을 지날 때 시간이 늦어 친구에게 연락하기에는 미안하고 부모님께 말씀드리기도 걱정하실까 망설여졌던 순간 다들 경험하셨을 겁니다..</p>
<p>이럴때 부담없이 이용할 수 있는 AI 귀갓길 동반자 서비스를 만들고 싶었어요</p>
<p>아이디어를 확정짓고 이미 조금 시작이 다른팀들에 비해 늦었다는 생각이 들어서 각자 맡은 문제 인식 / 서비스 소개 / 경쟁사 분석 / 수익화 모델 페이지를 다음날 오전까지 노션에 올리기로 하고 저희는 푸데푸데 잠에 들었습니다..</p>
<h1 id="3-서비스-개발과-ppt">3. 서비스 개발과 ppt</h1>
<p>서비스 개발은 앞서말했듯.. 빛재영님과 프론트의 떠오르는 샛별 아현이와 승민님이 해주셨습니다 진짜 고생하셨어요 특히 프론트분들은 저랑 같이 26학번이시고 처음 개발을 해보시는건 마찬가지인데 해내시는 모습보고 자극도 많이 받았습니다ㅜㅜ </p>
<p>저는 이 타이밍부터 ppt와 대본에 들어갈 자료조사를 시작했는데요 이때는 몰랐습니다... 저희가 제출 하루전에도 정보의 바다에서 해매게 될 줄은..</p>
<h1 id="4-ppt와-대본-메인기능-추가">4. ppt와 대본 메인기능 추가</h1>
<p>금요일부터 ppt 와이어프레임에 현서언니랑 같이 들어가서 페이지 구상을 도왔습니다!! 구상보다는 자료를 찾는데 저는 더 집중했던 것 같네요</p>
<p>그러던 중 AI를 이용한 서비스 부분이 약하다는 피드백이 돌아왔습니다.
명색이 &#39;AI를 사용해서 00를 없앤다면&#39;인데 저희 서비스에서 AI가 들어가는 곳은 AI와 하는 단계별 안심대화 뿐이었어요</p>
<p>더불어 수익화 모델도 너무 뻔하고 당연한 말들만이 쓰여있는 점,
궁극적으로 이 서비스를 통해 어떤 문제를 해결하고 싶은지도 명확하게 보이지 않는다는 점이 정말 큰 문제였습니다.</p>
<p>사실 저한테는 이 서비스로 어떤 문제를 해결하고 싶다는 게 있었는데 명확하게 설명을 해드리지 못한 것 같아 죄송한 마음도 있었습니다.</p>
<p>그래도 심사위원에게 까이느니 지금이라도 문제에 대해 인식했다는 것을 다행으로 여기고...</p>
<h4 id="그래서-결론은">그래서 결론은!!</h4>
<p>AI가 핵심으로 사용되는 기능을 하나 더 추가하자!!
근데 문제는 이 대화를 제출 약 24시간 전에 나누었다는 것이죠..</p>
<p>또한 저희가 정보는 많은데 정보들이 문제 하나에 대한 정보가 아니라 관련 정보를 다 모아놓은 느낌이고 정리가 안되어있는 것 같다고 하셔서
따로 정보를 찾고 모으는 게 아닌 한대의 컴퓨터로 2인 1조로 이야기를 나누며 자료를 찾고 어디에 자료를 넣으면 좋을지 기디분들에게 알려드리고 컨펌을 받는 방식으로 진행했습니다.</p>
<p>저는 AI핵심기능을 구상하는 쪽에 투입되었는데 정말 </p>
<h1 id="5-소감-및-느낀점">5. 소감 및 느낀점</h1>
<p>정말 너무너무 좋은 팀원분들을 만나서 다행이라는 생각이 많이 들었던 팀프로젝트엿습니다... 저희팀 너무 좋았어요</p>
<p>서로를 신경쓰고 폐를 끼치지 않으려고 노력하는 점이 다들 눈에 보였고.. 피드백을 할 때는 상대가 기분 나쁘지는 않을까 최대한 조심스럽게 말씀하려고 해주시고 피드백을 받을 때에는 부족한 점을 인정하고 받아들이시는 모습이 정말정말 인상깊었고 멋있었어요</p>
<p>제가 그리고 한번.. 잠들어서 디스코드 회의에 정<del>~</del>말 <strong>큰!!!</strong> 지각을 한 적이 있는데 저라면 조금은 언짢고.. 그랬을 것 같기도 한데 정말 다들 괜찮다고 해주시고 더 자야하는 거 아니냐고 걱정을 해주셔서 눈물이 날 뻔했습니다..</p>
<p>다들 잠을 못 주무셔서.. 정말 힘드셨을텐데 계속 피드백 해주시고 동기부여 해주시고 열정을 불어넣어주셔서 감사했어요</p>
<p>힘들면 다른 사람들 신경쓸 여유도 사라지고 짜증도 날 법한데 정말 팀에 웃음이 끊이지 않아서 신기했고 기술적인 부분에 대해서도 많이 배웠지만 이번에 저는 팀워크나 팀을 대하는 태도와 같은 부분에 대해서 정말 많이 배울 수 있었다고 생각이 듭니다..</p>
<p>첫 대회? 이벤트? 에서부터 너무 좋은 경험 쌓을 수 있어서 좋았습니다 멋사대학 모두 아자아자 화이팅!!!! 본선가자<del>~</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[<멋쟁이사자처럼> 5번째 벨로그]]></title>
            <link>https://velog.io/@j_tnqls311/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-5%EB%B2%88%EC%A7%B8-%EB%B2%A8%EB%A1%9C%EA%B7%B8</link>
            <guid>https://velog.io/@j_tnqls311/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-5%EB%B2%88%EC%A7%B8-%EB%B2%A8%EB%A1%9C%EA%B7%B8</guid>
            <pubDate>Sun, 10 May 2026 10:10:25 GMT</pubDate>
            <description><![CDATA[<h1 id="저번주-과제에-이어서">저번주 과제에 이어서..</h1>
<p>저번주 과제에서 다루지 못했던 캐시와 토큰에 대한 정리를 추가적으로 한 후에 5번째 벨로그를 본격적으로 작성해보겠습니다</p>
<h2 id="1-캐시">1. 캐시</h2>
<p>: 자주 사용되는 데이터를 더 빠르게 다시 사용하기 위해 임시로 저장해두는 저장 공간 or 저장 방식
=&gt; 매번 같은 데이터를 새로 불러오지 않고 한번 가져온 것을 잠시 저장해두었다가 필요할때 바로 꺼내 쓸 수 있음</p>
<h2 id="2-토큰">2. 토큰</h2>
<p>: 사용자의 인증 정보를 담고 있는 문자열 형태의 값
로그인 이후 서버가 사용자에게 토큰을 발급하면 사용자는 이휴 요청을 보낼 때 이 토큰을 함께 보냄. 서버는 토큰을 확인하여 사용자가 로그인한 사용자인지 판단</p>
<h1 id="1-관계형-데이터베이스">1. 관계형 데이터베이스</h1>
<h2 id="1-관계형-데이터베이스란">1. 관계형 데이터베이스란?</h2>
<p>: 데이터를 <strong>행(가로)</strong>과 <strong>열(세로)</strong>로 이루어진 <strong>테이블 형태</strong>로 저장하고 각 테이블 사이의 <strong>관계</strong>를 이용해 데이터를 관리하는 데이터베이스</p>
<p>서로 관련있는 데이터를 하나의 표에 저장하지 않고 여러개의 표로 나누어 저장한 뒤 기본 키와 외래 키를 이용하여 각 표를 연결한다.</p>
<ul>
<li>기본 키: 각 행을 다른 행과 구별할 수 있게 해주는 고유한 값</li>
<li>외래 키: 다른 테이블의 기본 키를 가져와서 테이블끼리 연결할 때 사용하는 값 </li>
</ul>
<h2 id="2-관계형-데이터베이스를-써야하는-이유">2. 관계형 데이터베이스를 써야하는 이유</h2>
<h4 id="1-데이터-중복을-줄일-수-있다">1. 데이터 중복을 줄일 수 있다.</h4>
<p>여러 정보를 한 표에 저장하면 정보가 중복될 수 있음
-&gt; 관계형 데이터베이스 사용시 데이터를 종류별로 나누어 저장하므로 이러한 중복을 줄일 수 있음</p>
<h4 id="2-수정이-편리하다">2. 수정이 편리하다.</h4>
<p>정보를 수정해야 할 때 정보가 여러 테이블에 반복되어있으면 수정이 어려움
-&gt; 하나의 테이블에서 가져다 쓰는 관계형 데이터베이스의 구조상 하나의 테이블에서만 정보를 수정하면 되므로 편라함</p>
<h4 id="3-필요한-정보를-조합하여-쓸-수-있다">3. 필요한 정보를 조합하여 쓸 수 있다.</h4>
<p>여러 테이블에 나누어 저장된 데이터를 공통된 속성을 기준으로 조인(Join) 하여 필요한 정보를 하나의 결과로 조회할 수 있음</p>
<h2 id="3-관계형-데이터베이스를-이해하기-위한-준비">3. 관계형 데이터베이스를 이해하기 위한 준비</h2>
<blockquote>
<p>데이터베이스
 └─ DBMS: 데이터베이스를 관리하는 소프트웨어
     ├─ RDBMS: 관계형 데이터베이스를 관리하는 DBMS
     └─ NoSQL DBMS: 관계형이 아닌 방식의 데이터베이스를 관리하는 DBMS
SQL: 주로 RDBMS와 대화할 때 사용하는 언어
ERD: 데이터베이스 구조를 설계할 때 그리는 다이어그램</p>
</blockquote>
<h4 id="1-dbmsdatabase-management-system">1. DBMS(Database Management System)</h4>
<p>: 사용자가 데이터베이스를 생성, 조회, 수정, 삭제하고 관리할 수 있게 해주는 소프트웨어</p>
<p>-&gt; 데이터베이스는 데이터를 저장하는 공간, DBMS는 그 데이터를 관리하게 해주는 프로그램!</p>
<p>EX) MySQL, MariaDB, Oracle Database, PostgreSQL, MongoDB
(MySQL, MariaDB, Oracle, PostgreSQL은 RDBMS /  MongoDB는 보통 NoSQL DBMS로 분류)</p>
<p><strong>무엇을 하나요?</strong></p>
<ul>
<li>데이터를 저장 및 조회 기능</li>
<li>여러 사용자의 동시접근을 가능하게 함</li>
<li>데이터의 무결성 유지, 여러사용자가 일관된 데이터를 볼 수 있도록 관리(= 데이터가 깨지지 않도록 한다?)</li>
<li>사용자별 여러 권한을 부여하고 보안을 관리
(예를 들어 관리자에게는 수정 및 삭제 등의 권한, 일반 사용자에게는 정보 조회 등의 권한을 부여한다)</li>
<li>에러 발생시 복구를 도움
(작업 내용을 기록해 두었다가 시스템이 비정상적으로 종료되더라도 다시 실행될 때 완료된 작업은 반영하고 완료되지 않은 작업은 되돌림으로써 데이터베이스를 정상적인 상태로 복구할 수 있게한다)</li>
</ul>
<h4 id="2-rdbms-relational-database-management-system">2. RDBMS( Relational Database Management System)</h4>
<p>: 관계형 데이터베이스를 관리하는 DBMS</p>
<p>DBMS는 데이터베이스를 관리하는 프로그램 전체 / RDMS는 그 중에서 관계형 데이터 모델을 따르는 데이터베이스를 관리하는 프로그램</p>
<p>EX) MySQL, MariaDB, PostgreSQL, Oracle Database, H2</p>
<h4 id="3-sqlstructured-query-language">3. SQL(Structured Query Language)</h4>
<p>: 관계형 데이터베이스의 데이터를 정의, 조작, 조회하기 위해 사용하는 언어
-&gt; RDBMS에게 명렁을 내릴 때 사용하는 언어</p>
<h4 id="4-erdentity-relationship-diagram">4. ERD(Entity-Relationship Diagram)</h4>
<p>: aka. 개체-관계 다이어그램, 데이터베이스를 설계할 때 어떤 데이터가 필요하고 그것들이 서로 어떤 관계를 가지는지 그림으로 표현한 것, 이를 수행하는 과정을 <strong>데이터 모델링</strong>이라고 함</p>
<p><strong>왜 해야할까?</strong>
관계형 데이터베이스는 여러 테이블 사이의 관계가 매우 중요하기 때문에 테이블을 만들기 전에 어떤 테이블이 필요한지, 기본 키는 어떤 것인지, 외래키는 어디에 둘지 등을 정하는 과정을 거쳐야한다.
이것을 시각적으로 표현하는 대표적인 도구가 ERD라고 할 수 있다.</p>
<h4 id="5-nosql">5. NoSQL</h4>
<p>: NoSQL은 일반적으로 전통적인 관계형 모델만을 따르지 않는 데이터베이스 방식을 뜻함. (Not only SQL) =&gt; 관계형 데이터베이스 외에도 다른 방식으로 데이터를 저장하고 처리하는 데이터베이스를 포괄적으로 지칭</p>
<p>저장 방식이 꼭 테이블일 필요가 없고 문서형, 그래프형 등으로 저장할 수 있다.
따라서 구조가 더욱 유연하다.</p>
<h1 id="2-내가-구현한-게시판의-erd">2. 내가 구현한 게시판의 ERD</h1>
<p>저번 벨로그에서 내가 구현한 게시판의 ERD를 작성해보겠다.</p>
<h2 id="1-erd-사용법-익히기">1. ERD 사용법 익히기</h2>
<p><a href="https://inpa.tistory.com/entry/DB-%F0%9F%93%9A-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AA%A8%EB%8D%B8%EB%A7%81-1N-%EA%B4%80%EA%B3%84-%F0%9F%93%88-ERD-%EB%8B%A4%EC%9D%B4%EC%96%B4%EA%B7%B8%EB%9E%A8">https://inpa.tistory.com/entry/DB-%F0%9F%93%9A-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AA%A8%EB%8D%B8%EB%A7%81-1N-%EA%B4%80%EA%B3%84-%F0%9F%93%88-ERD-%EB%8B%A4%EC%9D%B4%EC%96%B4%EA%B7%B8%EB%9E%A8</a>
나는 우선 이 글을 보고 관계형 데이터에 대한 기본적인 부분을 익혔다. </p>
<h4 id="1-개념적-데이터-모델링">1. 개념적 데이터 모델링</h4>
<p>우선 개념적 데이터 모델링을 해야한다. 이는 내가 하고자 하는 일의 데이터 간의 관계를 구상하는 단계로 각 개체들과 그들 간의 관계를 발견하고 표현하기 후에 ERD 다이어그램을 생성한다.</p>
<p><strong>피터 첸 표기법</strong>
: 도형이 의미하는 바를 알고 화살표를 통해 관계를 표현하는 방법
<img src="https://velog.velcdn.com/images/j_tnqls311/post/7b275c2c-e8f3-4b79-b89f-99357292d799/image.png" alt=""></p>
<h4 id="2-논리적-데이터-모델링">2. 논리적 데이터 모델링</h4>
<p>개념적 모델이 완성되고 구체화된 업무 중심의 데이터 모델을 만드는 것(표)
-&gt; 이 단계에서 업무에 대한 키 속성, 관계 등을 표시하며 정규화 활동을 수행한다. 정규화는 데이터 모델의 일관성을 확보하고 중복을 제거하여 신뢰성있는 데이터 구조를 얻기 위함이다.</p>
<h4 id="3-물리적-데이터-모델링">3. 물리적 데이터 모델링</h4>
<p>최종적으로 데이터를 관리할 데이터 베이스를 선택하고 선택한 데이터 베이스에 실제 테이블을 만드는 작업(SQL 코딩)
<img src="https://velog.velcdn.com/images/j_tnqls311/post/aebe175e-e6fd-4e55-8e5f-ca37b3da80df/image.png" alt=""></p>
<h4 id="4-erd-그리기---entity">4. ERD 그리기 - Entity</h4>
<p>Entity는 정의 가능한 사물 또는 개념을 의미한다.
-&gt; 데이터 베이스의 테이블이 엔티티로 표현됌</p>
<p>Entity 속성: Entity에는 개체가 가지고 있는 속성을 포함한다. 데이터베이스 테이블의 각 컬럼들이 Entity속성이 될 수 있다.</p>
<p>Entity 도메인: 속성의 값, 타입, 제약사항 등에 대한 값의 범위를 표현하는 것, 사용자의 기호에 따라 속성 타입만 둘수도, 가독성을 위해 생략할 수도 있음</p>
<p>Ex) 학생 Entity
<img src="https://velog.velcdn.com/images/j_tnqls311/post/f569cdb4-50c0-4fc2-93af-90883218eef4/image.png" alt="">
<img src="https://velog.velcdn.com/images/j_tnqls311/post/2a13530e-1ba6-42cd-8b6a-99a92fff3226/image.png" alt=""></p>
<p>Entity 분류: 저장하는 데이터 정보 주제에 따라 Entity의 종류가 달라짐, 각 데이터 주제에 맞게 모델링하기 위해 구분</p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/8b0a7360-73b0-4211-9dc9-1bbf4665dff4/image.png" alt=""></p>
<h4 id="5-edr-키와-제약조건-표기법">5. EDR 키와 제약조건 표기법</h4>
<p><strong>기본 키</strong>
중복이 없고 빈값이 없는 유일한 값에 지정</p>
<ul>
<li><p>아래와 같이 다이아몬드 표기 or 열쇠 표기 사용</p>
</li>
<li><p>유일한 속성이므로 다른 속성과의 명확한 구분을 위해 구분선을 두기도 함
<img src="https://velog.velcdn.com/images/j_tnqls311/post/45d9156c-4a49-44a8-ad48-bd897aaf2d7d/image.png" alt=""></p>
</li>
<li><p>속성에 들어갈 값에 null을 허용하지 않는 다면 N을 적고 허용한다면 N을 적지 않는다.</p>
</li>
</ul>
<p><strong>외래 키</strong>
키의 일종이기 때문에 보통 열쇠 아이콘 표기
외래 식별자를 표시할 때에는 선을 이어주는데 개체와 관계를 따져 표시함</p>
<h4 id="6-edr-entity-관계-표기법">6. EDR Entity 관계 표기법</h4>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/b94c831d-655d-40a8-b3e9-7148058a951d/image.png" alt="">
각 Entity 유형을 만든 후 Entity끼리 관계가 있다면 선을 이어 관계를 맺어야 함.</p>
<ul>
<li>실선: 강한 관계(식별자 관계), 부모엔티티 종속</li>
<li>점선: 약한 관계(비식별자 관계), 종속 약함</li>
</ul>
<blockquote>
<p>What is 식별자 관계?
식별자 관계: 부모자식 관계에서 부모의 기본 키를 외래 키로 차ㅁ조해서 자신의 기본 키로 설정
<img src="https://velog.velcdn.com/images/j_tnqls311/post/3fd44a84-c68a-4175-adec-e4590d9eed49/image.png" alt="">
비식별자 관계: 부모자식 관계에서 자식이 부모의 기본 키를 외래 키로 참조하여 일반 속성으로 사용
<img src="https://velog.velcdn.com/images/j_tnqls311/post/546607be-60ca-4eb9-9dcf-39ebebda50b6/image.png" alt=""></p>
</blockquote>
<h4 id="7-edr관계의-cardinality관계의-개수">7. EDR관계의 Cardinality(관계의 개수)</h4>
<p>1대 1대응 / 1대 다대응 / 다대 1대응 / 다대 다대응
<img src="https://velog.velcdn.com/images/j_tnqls311/post/285779ce-bdaa-427e-a391-7072d84b7702/image.png" alt="">
위처럼 그냥 선을 긋게 된다면 가독성이 매우 안좋아질 수 있음
-&gt; 선의 끝모양을 다르게 표시</p>
<p>1) 1대 1대응
<img src="https://velog.velcdn.com/images/j_tnqls311/post/e8bb0ec3-8000-4d6e-9760-4a025757ac85/image.png" alt="">
2) 1대 다대응
<img src="https://velog.velcdn.com/images/j_tnqls311/post/2a31e3ae-5862-4e03-aee8-7a8db7c88f2a/image.png" alt="">
3) 다대 1대응</p>
<p>4) 다대 다대응
<img src="https://velog.velcdn.com/images/j_tnqls311/post/0ef18673-23fe-4d2e-a37f-26f671586186/image.png" alt=""></p>
<h4 id="8-1-edr-관계의-참여도">8-1. EDR 관계의 참여도</h4>
<p>: 관계도 각 측의 끝자락에 기호 표시, <strong>&quot;ㅣ&quot;</strong> 표시가 있는 곳은 반드시 있어야함 
o 표시가 있는 곳은 없어도 됌</p>
<h4 id="8-2-관계의-필수기호">8-2. 관계의 필수기호</h4>
<p>어느한쪽이 존재하면 다른 쪽도 반드시 존재해야 하는 관계
<img src="https://velog.velcdn.com/images/j_tnqls311/post/0de7a762-533a-4e61-b6fd-44cf14f9f4de/image.png" alt=""></p>
<p>Ex) 학번 21003 학생의 취미가 낚시 라는 정보가 있다면, 21003학번의 학생의 정보가 학생 엔티티에 반드시 존재해야 한다.</p>
<h2 id="2-erdcloud를-이용하여-게시판-erd-작성하기">2. ERDcloud를 이용하여 게시판 ERD 작성하기</h2>
<h4 id="1-erd사용법-익히기">1. ERD사용법 익히기</h4>
<p>나는 아래의 글을 참고하여 기본적인 EDRcloud 사용법을 익혔다.
<a href="https://cinnamonlover.tistory.com/entry/ERD-%EC%82%AC%EC%9A%A9%EB%B2%95-%EB%AF%B8%EC%99%84%EC%84%B1">https://cinnamonlover.tistory.com/entry/ERD-%EC%82%AC%EC%9A%A9%EB%B2%95-%EB%AF%B8%EC%99%84%EC%84%B1</a></p>
<p>기본적인 Entity 등록법도 헤매어서 글 참고를 많이 했다
<img src="https://velog.velcdn.com/images/j_tnqls311/post/6c952a5e-0040-4c94-ab64-cb46aa70214a/image.png" alt=""></p>
<h4 id="2-erd-그리기">2. ERD 그리기</h4>
<blockquote>
<h3 id="문제점-1">문제점 1.</h3>
<p>어떻게 ERD를 작성해야 할지 몰라서 GPT에게 물어보니 내가 작성한 코드에는 ERD가 없다는 답변을 들을 수 있었다.
그래서 일단 관계가 없는 내 게시판 코드의 ERD도 작성하고 GPT가 알려준 일반적인 게시판 ERD 또한 작성해 보았다.</p>
</blockquote>
<h4 id="3-내가-구현한-게시판-edr관계x">3. 내가 구현한 게시판 EDR(관계x)</h4>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/919928e4-5d20-4f45-bd5c-bc359779d922/image.png" alt=""></p>
<h4 id="4-일반적인-게시판-edr관계o">4. 일반적인 게시판 EDR(관계o)</h4>
<p>일반적으로 회원 1명은 게시글을 여러개 작성할 수 있으며 게시글 1개는 회원 1명이 작성하므로 1(회원)대 다(게시글) 관계이다.</p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/abaf200b-5083-436f-ab4e-445a6d07c3cf/image.png" alt=""></p>
<p>부모 테이블의 기본키가 자식 테이블의 외래키로만 들어가고 자식 테이블의 기본키에는 포함되지 않는 관계이므로 Non-identifying Relationship 설정 = 비식별 관계</p>
<h1 id="3-postman을-이용한-api-테스트">3. Postman을 이용한 API 테스트</h1>
<h2 id="1-postman이란">1. Postman이란?</h2>
<p>: API를 설계, 요청, 테스트, 디버깅, 문서화, 공유하기 위한 API개발 플랫폼
-&gt; 개발자가 직접 코드를 작성하거나 브라우저 화면을 만들지 않아도 HTTP요청을 보내고 서버의 응답을 확인할 수 있게 해주는 API 클라이언트 도구</p>
<h2 id="2-api란">2. API란?</h2>
<p>: 프로그램과 프로그램이 서로 약속된 방식으로 데이터를 주고받는 통로</p>
<p>Ex) 쇼핑물 사이트에서 사용자가 상품 목록 페이지에 들어가면 브라우저가 서버에게 상품 목록을 요청한다.
이때 브라우저와 서버가 데이터를 주고받는 약속된 주소가 API이다.</p>
<h2 id="3-postman은-무엇을-대신해주는가">3. PostMan은 무엇을 대신해주는가?</h2>
<p>서버 API를 테스트하기 위해 프론트엔드 화면이 필요할 수 있다.
예를 들어 스프링 부트로 상품 등록 기능을 만들었을 때, 
이 API가 잘 작동하는지 확인하려면 HTML 입력창을 만들고 버튼을 만들고 서버로 데이터가 전송되도록 해야한다. </p>
<p>BUT. Postman을 쓰면 HTML화면 없이도 바로 서버에 요청을 보낼 수 있다.
-&gt; API가 제대로 동작하는지 빠르게 확인할 수 있다.</p>
<blockquote>
<h4 id="postman이-유용한-상황">Postman이 유용한 상황</h4>
</blockquote>
<ol>
<li>프론트엔드 화면이 아직 구현되지 않았을 때</li>
<li>에러가 프론트 문제인지, 백엔드 문제인지 구분할 때</li>
<li>여러 기능을 각각 따로 테스트해 볼 때</li>
</ol>
<h2 id="4-내가-구현한-게시판-postman으로-테스트-해보기">4. 내가 구현한 게시판 Postman으로 테스트 해보기</h2>
<blockquote>
<h3 id="문제점-2">문제점 2.</h3>
<p>이걸 문제점이라고 해도 되는지 모르겠지만ㅜㅜ
Postman 실행이 안된다 아무래도 내 노트북의 문제인 거 같은데 과제 제출시간은 다가오고..?? 실행은 안돼고.. 어쩔 수 없이 웹사이트로 들어가서 과제를 수행하려고 했는데ㅜㅜ localhost는 나만 들어갈 수 있대서.. 웹에서 실행이 되지 않았다ㅜㅜ 
내가 만든 게시판의 API는 테스트하지 못했고 Postman의 사용법을 익히기로 하였다(다음주 벨로그에 API테스트하는 과정까지 올리겠습니다💧)</p>
</blockquote>
<p>나는 아래의 글을 통해 Postman 사용법을 익혔다.
<a href="https://gganmu.tistory.com/27#num3">https://gganmu.tistory.com/27#num3</a></p>
<h4 id="1-get-요청-리소스-조회read">1. GET 요청: 리소스 조회(Read)</h4>
<p>: 데이터를 <strong>조회</strong>할 때 쓰는 요청 방식
게시판을 예로 들면</p>
<ol>
<li>게시판 글 목록 보기</li>
<li>회원 정보 조회하기</li>
<li>특정 게시글 보기
등의 작업을 GET을 통해 수행한다.</li>
</ol>
<blockquote>
<p><strong>Postman에서 GET 쓰는 법</strong>
<img src="https://velog.velcdn.com/images/j_tnqls311/post/5ed3a7a7-51ec-4b84-8f36-7b732a29e033/image.png" alt="">
위쪽 요청방식을 GET으로 설정한 후에 URL을 입력한 후 Send 버튼을 누른다.
-&gt; Body란에 200 ok(요청 정상 처리)가 뜨고 데이터가 반환되었다면 성공</p>
</blockquote>
<blockquote>
<p><strong>특정 유저가 쓴 게시글 조회(조건 붙이기)</strong>
=&gt; Query Param 사용
<img src="https://velog.velcdn.com/images/j_tnqls311/post/6d4f460f-c3f3-45ab-b637-fb1c833b5f21/image.png" alt="">
Key는 userId, Value는 10을 입력하면 10번 유저가 쓴 게시글을 조회할 수 있다.</p>
</blockquote>
<h4 id="2-post-요청-새-리소스-생성create">2. POST 요청: 새 리소스 생성(Create)</h4>
<p>: 서버에 새로운 데이터를 보낼 때 쓰는 요청 방식
게시판을 예로 들면</p>
<ol>
<li>게시글 작성하기</li>
<li>회원가입하기</li>
<li>로그인하기</li>
<li>댓글작성하기</li>
</ol>
<blockquote>
<p><strong>Postman에서 POST 쓰는 법</strong>
<img src="https://velog.velcdn.com/images/j_tnqls311/post/cdd18c6f-986a-4f84-9523-1f883486736a/image.png" alt="">
위쪽 요청방식을 POST으로 설정하고 URL을 입력, raw &gt; JSON 선택 후 데이터 입력을 마치고 Send
-&gt; 201 Created(요청 생성됨)은 리소스 생성 성공 시, 생성된 리소스의 정보를 json 형태로 반환함</p>
</blockquote>
<h4 id="3-put-요청-리소스-전체-교체update">3. PUT 요청: 리소스 전체 교체(Update)</h4>
<p>: 기존 데이터를 <strong>전체 수정</strong>할 때 쓰는 요청 방식
-&gt; 이 번호의 데이터를 내가 보낸 내용으로 바꿔줘</p>
<blockquote>
<p><strong>Postman에서 PUT 쓰는 법</strong>
<img src="https://velog.velcdn.com/images/j_tnqls311/post/dd0408fc-d581-4159-9a6e-888ede50464c/image.png" alt="">
위쪽 요청방식을 PUT으로 설정하고 URL을 입력, raw &gt; JSON 선택 후 데이터 수정을 마치고 Send</p>
</blockquote>
<h4 id="4-patch-요청-리소스-일부-수정partial-update">4. PATCH 요청: 리소스 일부 수정(Partial Update)</h4>
<p>: 기존 데이터의 <strong>일부분</strong>을 수정할 때 사용</p>
<p>게시판을 예로 들면, 게시판의 제목, 내용, 태그 등의 내용 중 제목만 수정하고 싶을 때 PATCH를 쓸 수 있다.</p>
<blockquote>
<p>PUT과 PATCH의 차이
PUT은 body에 titlem content, tag 모두를 보내지만 PATCH는 수정할 title 필드만을 보낸다.</p>
</blockquote>
<blockquote>
<p><strong>Postman에서 PATCH 쓰는 법</strong>
<img src="https://velog.velcdn.com/images/j_tnqls311/post/9e9c4a15-f6fe-4cf9-80a1-fabd3565a9f9/image.png" alt="">
위쪽 요청방식을 PATCH으로 설정하고 URL을 입력, raw &gt; JSON 선택 후 일부 데이터 수정을 마치고 Send</p>
</blockquote>
<h4 id="5-delete-요청-리소스-삭제delete">5. DELETE 요청: 리소스 삭제(Delete)</h4>
<p>: 기존 데이터를 삭제할 때 쓰는 요청 방식</p>
<blockquote>
<p><strong>Postman에서 DELETE 쓰는 법</strong>
<img src="https://velog.velcdn.com/images/j_tnqls311/post/a08db912-8240-4305-b664-d6712fdff89c/image.png" alt="">
위쪽 요청방식을 DELETE으로 설정하고 URL을 입력 후 send</p>
</blockquote>
<h1 id="4-느낀점-및-소감">4. 느낀점 및 소감</h1>
<p>컴퓨터는 언어가 왜이렇게 많은걸까......??
SQL 또 할 생각에 머리가 아프다ㅜ 언어 통일해주면 안될까?😂</p>
<p>원래 항상 지인님이 올려주신 영상을 보면서 공부했는데 이번 주 ERD는 초보자가 듣기에 조금 추상적일수도 있다고 해서 다른 방법을 찾아 공부를 해보았다!! 내가 잘 이해하고 따라갈 수 있는 설명을 찾는 것이 쉽지 않다는 것을 알게되었다. (지인님 리스펙)</p>
<p>그리고 왜자꾸 ERD를 EDR이라고 하지?.. GPT한테 뭐 물어볼때마다 자꾸 EDR이라고 하니까 GPT가 계속 정정해준다</p>
<p>이번주차 과제는 하면서 기분이 좋았던?? 과제 중에 하나인 것 같다 원래 따라가기 급급해져서 그로그램의 구조는 생각해본 적이 거의 없는데 이렇게 과제를 통해 ERD를 배우니 머릿속에 틀이 잡혀나가는 느낌이라 좋았다.</p>
<p>좋았는데 정말 좋았는데...... 왜 Postman 안되는거지..? 아무래도 노트북 바꿔야겠다..</p>
<p>요즘 아이디어톤을 하기 시작해서 다들 너무 바쁠것 같다는 생각이 들어요 멋사대학 모두 화이팅!! 돌아보면 분명 뿌듯할 겁니다 저는 지금 제가 쓴 벨로그만 봐도 좀 뿌듯하거든요..ㅎㅎㅎㅎ 별건 없지만 이런 성취감과 뿌듯함이 저에게는 계속 공부할 수 있게 해주는 원동력이 되는 것 같습니다 아자아자🔥🦁</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[<멋쟁이사자처럼> 4번째 벨로그]]></title>
            <link>https://velog.io/@j_tnqls311/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-4%EB%B2%88%EC%A7%B8-%EB%B2%A8%EB%A1%9C%EA%B7%B8</link>
            <guid>https://velog.io/@j_tnqls311/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-4%EB%B2%88%EC%A7%B8-%EB%B2%A8%EB%A1%9C%EA%B7%B8</guid>
            <pubDate>Tue, 28 Apr 2026 05:26:44 GMT</pubDate>
            <description><![CDATA[<h1 id="1-로그인-구현-종류">1. 로그인 구현 종류</h1>
<h2 id="1-쿠키-vs-세션">1) 쿠키 vs 세션</h2>
<h4 id="1-쿠키">1) 쿠키</h4>
<p>: 브라우저에 정보를 저장할 수 있는 작은 공간
-&gt; 사용자 식별 아이디, 설정값(다크모드 설정), 
BUT. 비밀번호와 같은 개인정보는 넣으면 안됌 
<strong>사용자가 변경가능하여 취약함</strong></p>
<h4 id="2-세션">2) 세션</h4>
<p>:서버가 특정 클라이언트와의 연속적인 상호작용 상태를 식별하고 유지하기 위해 저장하는 서버 측 상태 정보, 로그인 요청이 들어올 때마다 DB에서 일치하는 세션아이디를 확인
-&gt; 사용자가 값을 바꿀 수 없음 사용자 브라우저가 아닌 서버에서 확인하는 용도
세션 식별아이디가 영어와 숫자를 랜덤하게 조합한 것이라고는 하나 해킹의 위험이 있음</p>
<p>3) JWT
: 사용자의 정보를 인코딩하여 토큰으로 활용한 것
(DB 필요 없음, 조작하지 못하게 처리되어 있음)
-&gt; 정보를 인코딩한 것이기 때문에 디코딩하면 모든 정보를 읽을 수 있음</p>
<h2 id="2-세션-로그인">2) 세션 로그인</h2>
<ol>
<li>로그인 요청 -&gt; 사용자가 아이디와 비번을 입력</li>
<li>서버 검증 -&gt; 서버가 DB에 저장된 정보와 비교</li>
<li>세션 생성 -&gt; 서버가 세션 ID생성</li>
<li>쿠키에 세션 ID저장 -&gt; 브라우저의 쿠키에 세션 ID 넣어줌</li>
<li>이후 요청 -&gt; 이후 페이지에 요청할 때마다 쿠키의 세션 ID 자동 전송</li>
<li>서버가 로그인 상태 확인 -&gt; 세션 ID를 보고 로그인 했던 전적 확인</li>
</ol>
<blockquote>
<p>단점</p>
</blockquote>
<ul>
<li>서버에 저장해야 하기 때문에 서버 DB문제가 있을 수 있음</li>
</ul>
<h2 id="3-jwt-로그인">3) JWT 로그인</h2>
<ol>
<li>로그인 요청 -&gt; 사용자가 아이디와 비번을 입력</li>
<li>서버 검증 -&gt; 서버가 DB에 저장된 정보와 비교</li>
<li>JWT 생성 -&gt; 서버가 JWT 토큰 생성 (사용자 정보 + 만료시간 + 서버 서명 포함)</li>
<li>클라이언트 저장 -&gt; 서버가 JWT를 사용자에게 전달</li>
<li>이후 요청 -&gt; 사용자가 요청할때마다 JWT 토큰 서버 전송</li>
<li>서버가 검증 -&gt; JWT 받아서 확인</li>
</ol>
<blockquote>
<p>단점 </p>
</blockquote>
<ul>
<li>토큰이 탈취될 가능성이 있음</li>
</ul>
<h2 id="4-oauth-로그인">4) OAuth 로그인</h2>
<ol>
<li>로그인 요청 -&gt; 사용자가 “구글로 로그인”, “카카오로 로그인” 클릭</li>
<li>인증 서버 이동 -&gt; 서비스가 사용자를 구글/카카오 로그인 페이지로 보냄</li>
<li>사용자 로그인 -&gt; 사용자가 해당 서비스에서 아이디/비밀번호 입력</li>
<li>인증 코드 받기 -&gt; 로그인 성공하면 인증 서버가 기존 서버에게 임시 코드 전달</li>
<li>토큰 요청 -&gt; 기존 서버가 인증 서버에게 인증코드, 클라이언트 아이디, 클라이언트 시크릿 등 정보를 줌</li>
<li>토큰 발급 -&gt; 인증 서버가 확인 후 엑세스 토큰 발급</li>
<li>사용자 정보 요청 -&gt; 우리 서버가 토큰으로 사용자 정보 요청</li>
<li>로그인 -&gt; 처리회원가입 or 로그인 처리 완료</li>
</ol>
<blockquote>
<p>단점</p>
</blockquote>
<ul>
<li>외부 서비스 의존</li>
<li>코드 구현이 복잡함</li>
</ul>
<h1 id="2-내가-채택한-방법과-구현과정">2. 내가 채택한 방법과 구현과정</h1>
<h2 id="1-내가-택한-로그인-구현방법">1) 내가 택한 로그인 구현방법</h2>
<p>내가 채택한 로그인 구현방법은 &quot;세션로그인&quot;이다
그 이유는 다음과 같다. </p>
<ol>
<li><p>복잡성
시험이 끝나고 시간분배를 잘 했어야 했는데 그러지 못하고 너무 오래 놀았습니다..... 시간이 없는 초보자가 그나마 안정적으로 성공할 수 있는 가능성이 보인 로그인 구현 방법이 세션로그인이라고 생각했습니다. 그래서 JWT, OAuth 보다 비교적 간단한 세션 로그인을 선택했습니다.</p>
</li>
<li><p>기본기 이해
JWT와 OAuth 모두 세션 로그인에서 비롯되어 발전된 구조이므로 세션 로그인 구조를 이해하면 나중에 다른 로그인 구조를 이해하기 훨씬 수월할 것이라고 생각하였습니다. </p>
</li>
</ol>
<h2 id="2-게시판-페이지-구현">2) 게시판 페이지 구현</h2>
<h3 id="step-1-기본-파일-실행시키기">STEP 1. 기본 파일 실행시키기</h3>
<blockquote>
<h2 id="문제점-1">문제점 1.</h2>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/d41b67bd-5234-4814-b86d-855e3851c25e/image.png" alt="">
전주차에 구현했던 상품 목록 관리 파일이 날아가서 스프링부터 다시 깔고 처음부터 하고 있었다.😂
저번에는 404 NOT FOUND 에러가 나더니 이번에는 Whitelabel 에러가 떴다...
시간이 없어 일단 유튜브를 보며 설명을 따라갔다ㅜㅜ
<img src="https://velog.velcdn.com/images/j_tnqls311/post/a9ffa679-ed25-475b-8f37-c4e2090fb7b6/image.png" alt="">
저번처럼 실행하는 곳(?)이 잘못되었나 싶기도 했는데 강사님이랑 똑같이 Current file로 실행하는데 안되는 이유를 모르겠다ㅜㅜ
<img src="https://velog.velcdn.com/images/j_tnqls311/post/245b36f5-c54e-4599-930d-a1761b9e65ba/image.png" alt="">
다른 부분을 하다보니까 저 problem이 자꾸 눈에 거슬려서 GPT에게 해결을 어떻게 하냐고 물어보았다. 
<img src="https://velog.velcdn.com/images/j_tnqls311/post/ce8770c7-fc63-4f07-8641-1e241592e21e/image.png" alt="">
그랬더니 Thymleaf 의존성이 없을 가능성이 있다고 해서 defendencies에 저 코드를 추가했더니
<img src="https://velog.velcdn.com/images/j_tnqls311/post/c7a498d4-00b9-46ba-994f-2d4fd4bd5945/image.png" alt="">
실행이되었다.....
되긴 했는데 어떻게 된건지 모르겠다 다음에 Whitelabel 에러가 또 떴을 때 해결할 자신이 없다 어떻게 된걸까..?? 난 분명 springBoot 파일을 다운받을 때 Thymleaf 기능도 추가했는데 왜 defendencies에 없었던 걸까</p>
</blockquote>
<h3 id="step-2-글작성-화면-구현하기">STEP 2. 글작성 화면 구현하기</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/6b17b90c-59f1-4f0b-8a80-1e75e3b5ceb5/image.png" alt="">
<img src="https://velog.velcdn.com/images/j_tnqls311/post/2d837fb5-7e58-4c23-a887-752df734ea7a/image.png" alt="">
HTML은 깃허브에 올라온 것을 복사해서 사용하였고 Controller는 강사님의 설명을 보고 따라치면서 기억나지 않는 부분을 복습하며 공부하였다. 
다행히 이 부분에서는 막히지 않았다</p>
<blockquote>
<h2 id="문제점-2">문제점 2.</h2>
<p>@Getter @Setter를 쓰려는데 어노테이션이 뜨지 않아서 강사님 영상을 확인하니 나는 롬북 어노테이션이 뜨지 않는다는 것을 발견했다.
이것도 Thymleaf와 비슷한 문제인가 생각이 들었다. 
<img src="https://velog.velcdn.com/images/j_tnqls311/post/3d9697c2-eae2-4658-a092-55463f327843/image.png" alt="">
롬북도 Thymleaf와 마찬가지로 defendencies가 없었다. 이번에는 그래도 빠르게 GPT에게 물어보고 의존성을 추가했다. </p>
</blockquote>
<h3 id="step3-maria-db-연동하기">STEP3. Maria DB 연동하기</h3>
<p><a href="https://www.youtube.com/watch?v=QRqWuJkpWRw&amp;t=3s">https://www.youtube.com/watch?v=QRqWuJkpWRw&amp;t=3s</a>
<a href="https://www.youtube.com/watch?v=ZnxHLyu0Pso&amp;t=213s">https://www.youtube.com/watch?v=ZnxHLyu0Pso&amp;t=213s</a>
나는 위 두 동영상을 참고하여 Maria DB를 인텔리제이에 연동하였다. 
저번과는 다르게 한번 해보아서인지 그래도 수월하게 할 수 있었다!!
<img src="https://velog.velcdn.com/images/j_tnqls311/post/6c4e43a5-ecbe-4a7c-910b-638bf05e7a08/image.png" alt=""></p>
<blockquote>
<h4 id="yml-vs-properties">yml vs properties</h4>
<p>강의에서 application.properties를 application.yml로 바꾸었는데 강사님은 다른 DB를 쓰고 나는 Maria DB를 써서 안바꾸면 문제가 될까 싶어 properties와 yml 확장자의 차이를 알아보게 되었다. </p>
</blockquote>
<ul>
<li>properies: 한 줄씩 key=value로 쓰는 방식<pre><code>spring.application.name=demo
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3307/board
spring.datasource.username=root
spring.datasource.password=7470
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true</code></pre></li>
<li>yml: 들여쓰기로 구조를 표현하는 방식<pre><code>spring:
application:
  name: demo
datasource:
  driver-class-name: org.mariadb.jdbc.Driver
  url: jdbc:mariadb://localhost:3307/board
  username: root
  password: 7470
jpa:
  hibernate:
    ddl-auto: update
  show-sql: true
  properties:
    hibernate:
      format_sql: true</code></pre></li>
</ul>
<h3 id="step4-글목록-기능-백엔드-구현">STEP4. 글목록 기능 백엔드 구현</h3>
<p>Controller → Service → Repository → DB→ Repository → Service → Controller </p>
<p>먼저 DB에 있는 글목록을 꺼내 사용자에게 보이기 위해 Service에서 findAll을 사용하여 정보를 꺼냈다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/c5ec6c67-a8d9-47c7-bb51-2c0072387297/image.png" alt=""></p>
<p>이후 DB에 저장되어 있던 Entity들을 DTO로 바꾸어주는 코드를 BoardDTO파일에 작성하였다.</p>
<blockquote>
<h4 id="dto-vs-entity">DTO VS Entity</h4>
<p>Repositiry는 주로 Entity 형태의 값을 DB에서 저장 및 삭제하고 Service는 Repository에게 받은 이 값을 DTO 값으로 바꾸어서 처리한다.
DTO와 Entity는 무엇이 다른 것일까?</p>
</blockquote>
<h4 id="--dtodata-transfer-object-화면요청응답-기준-데이터">- DTO(Data Transfer Object): 화면/요청/응답 기준 데이터</h4>
<h4 id="--entity-db-저장소-기준-데이터">- Entity: DB 저장소 기준 데이터</h4>
<p>예를 들어 사용자에게 보여주어야 할 정보가 아닌 것도 있을 수 있으므로 사용자에게 보여줄 정보는 DTO를 DB에만 저장해도 되는 정보, 민감한 정보들은 Entity를 사용하여 따로 DB에 저장할 수 있는 것이다.</p>
<p>마지막으로 Controller를 사용하여 목록 정보를 화면으로 보내는 코드를 작성했다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/a66c51cb-13f8-4fe7-b336-cd7b9b271154/image.png" alt=""></p>
<h3 id="step5-글목록-기능-화면구현">STEP5. 글목록 기능 화면구현</h3>
<p>여기서도 HTML은 깃허브에 올라와있는 코드를 복사해서 사용했다. 
<img src="https://velog.velcdn.com/images/j_tnqls311/post/9ecb1260-aec5-4555-b27e-bd513c0bc951/image.png" alt="">
위와같이 게시판이 잘 작동되는 것을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/38632141-e196-40af-9873-d78db3554ca3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/264d6e4d-b82b-49a7-bf95-9c2a7ded43cd/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/ee7c31aa-e027-46bb-8b61-8d1da0602b80/image.png" alt="">
다만 아직 조회기능을 만들지 못하였기 때문에 글목록에 들어가면 Whitelabel 에러메세지가 나타난다.</p>
<h3 id="step6-글조회-기능-백엔드-구현">STEP6. 글조회 기능 백엔드 구현</h3>
<p>Controller에서 id 값을 조회하고 Service에게 요청하면 Service가 Repository에서 Entity값을 DTO로 바꾸어 Controller에게 전달하고 COntroller는 그것을 HTML을 통하여 사용자에게 보여준다. 
<img src="https://velog.velcdn.com/images/j_tnqls311/post/c33d39de-67b9-4b95-be48-ec095efc12c5/image.png" alt="">
<img src="https://velog.velcdn.com/images/j_tnqls311/post/98a4d746-cae4-4c62-a026-17d26dc4191a/image.png" alt="">
<img src="https://velog.velcdn.com/images/j_tnqls311/post/b10415d3-4d0e-4b7a-a4ad-0345b1c76ead/image.png" alt=""></p>
<h3 id="step7-글조회-기능-화면구현">STEP7. 글조회 기능 화면구현</h3>
<blockquote>
<h2 id="문제점-3">문제점 3.</h2>
<p>백엔드 구현을 하고 HTML을 복사 붙여넣기 했을 뿐인데 글조회 기능이 실행되지 않았다. 
<img src="https://velog.velcdn.com/images/j_tnqls311/post/d88463f8-02a2-4e2f-9151-57578dbba02c/image.png" alt="">
HTMl이 잘못되었을리는 없어서 GPT에게 에러코드를 보여주었더니 Contrller에서 id를 넘겨주는 파람 어노테이션이 없을거라는 답변을 들었다. 그래서 파람이 있다고 화면을 보여주었더니 그 위에 줄 띄어쓰기가 잘못되어서 실행이 되지 않은 거였다.. <del>이걸 어떻게 알아</del></p>
</blockquote>
<p>다행히 띄어쓰기 잘 고치고 글 조회기능을 만들 수 있었다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/c726e824-e7d8-4d4d-a7f7-567761103b45/image.png" alt="">
<img src="https://velog.velcdn.com/images/j_tnqls311/post/64c26b64-60fe-4289-815c-bf565c7fea6e/image.png" alt=""></p>
<h3 id="step8-글삭제-기능-백엔드화면구현">STEP8. 글삭제 기능 백엔드&amp;화면구현</h3>
<p>Controller와 Service, HTML에 약간의 코드만 추가하면 되는 기능이므로 수월하게 구현할 수 있었다.
다른 정보 필요없이 id만 있으면 그 안에 들어있는 정보는 DB가 알아서 삭제해준다는 점이 신기했다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/34cd61f8-abb5-43c9-a450-14c7799c4864/image.png" alt=""></p>
<h3 id="step9-글수정-기능-화면구현">STEP9. 글수정 기능 화면구현</h3>
<h3 id="step10-글수정-기능-백엔드-구현">STEP10. 글수정 기능 백엔드 구현</h3>
<p>시간부족으로 아직 구현하지 못했습니다...............</p>
<h2 id="3-로그인-구현">3) 로그인 구현</h2>
<h3 id="step1-로그인-기능-구조">STEP1. 로그인 기능 구조</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/7313033f-479c-4522-b83f-d98938221c49/image.png" alt=""></p>
<blockquote>
</blockquote>
<ul>
<li>회원가입:
사용자입력값 -&gt; MemberDTO -&gt; Memverentity-&gt; DB저장</li>
<li>로그인:
사용자입력값 -&gt; 이메일로 회원 조회 -&gt; 비밀번호 비교 -&gt; 세션 저장</li>
<li>로그아웃:
세션 삭제</li>
</ul>
<h3 id="step2-repository와-db-접근-준비">STEP2. Repository와 DB 접근 준비</h3>
<p>JpaRepository를 상속받아 회원 저장과 조회 기능을 사용할 수 있도록 했다. 로그인할 때는 이메일로 회원을 찾아야 하므로 findByMemberEmail 메서드를 추가했다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/c69fb774-08eb-4824-9ead-49ada422abb5/image.png" alt="">
<img src="https://velog.velcdn.com/images/j_tnqls311/post/2641edbd-6dd0-444a-a8a2-c2c39e51c564/image.png" alt="">
MemberEntity는 DB의 member_table과 연결되는 클래스로 회원의 이메일, 비밀번호, 이름 정보를 DB에 저장하기 위한 실제 테이블 구조 역할이다.</p>
<h3 id="step3-회원가입-기능-구현하기">STEP3. 회원가입 기능 구현하기</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/2f851902-92c8-437e-bcbd-1517879c99a9/image.png" alt=""></p>
<blockquote>
<h4 id="흐름">흐름</h4>
<p><a href="http://localhost:8080/member/save">http://localhost:8080/member/save</a> 들어감
회원가입 폼 풀력
사용자 입력
MemberDTO 받음
MemberEntity로 변환
DB저장</p>
</blockquote>
<p>회원가입 요청은 Controller에서 먼저 받는다. 사용자가 입력한 이메일, 비밀번호, 이름은 MemberDTO에 담기고, Service에서 Entity로 변환한 뒤 Repository를 통해 DB에 저장했다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/cc6b6f70-4103-4f2f-b206-185ce141fd24/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/fc2f6c2e-09a2-40d5-9515-17f67097f7e2/image.png" alt=""></p>
<pre><code>public static MemberEntity toMemberEntity(MemberDTO memberDTO)</code></pre><p>화면에서 전달받은 DTO 데이터를 DB에 저장할 수 있는 Entity 형태로 변환하는 메서드</p>
<h3 id="step4-로그인-기능-구현하기">STEP4. 로그인 기능 구현하기</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/8b3c79aa-fc05-46f1-9766-b29d86002ca4/image.png" alt=""></p>
<blockquote>
<h4 id="흐름-1">흐름</h4>
<p><a href="http://localhost:8080/member/login">http://localhost:8080/member/login</a> 들어감
로그인 폼 출력
이메일, 비번 입력
이메일로 DB 조회
비밀번호 비교
성공 시 세션 저장</p>
</blockquote>
<p>로그인에서는 사용자가 입력한 이메일을 기준으로 DB에서 회원을 조회했다. 회원이 존재하면 입력한 비밀번호와 DB에 저장된 비밀번호를 비교했고, 일치할 경우 로그인 성공으로 판단했다.</p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/50417d09-75e7-40f9-a57c-b034e3248b29/image.png" alt=""></p>
<p>Optional = null 에러를 줄이기 위해, 값이 있을 수도/없을 수도 있음을 표현하는 상자
findByMemberEmail의 결과는 회원이 없을 수도 있기 때문에 isPresent()를 이용해 회원 존재 여부를 먼저 확인하고 Optional로 값을받았다.</p>
<h3 id="step5-세션에-로그인-정보-저장하기">STEP5. 세션에 로그인 정보 저장하기</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/491d8741-c60c-4325-8d10-0da7e90f1b45/image.png" alt=""></p>
<p>로그인 성공 후에는 <code>HttpSession</code>에 사용자의 이메일과 이름을 저장했다. 세션에 값이 저장되어 있으면 서버는 이후 요청에서도 해당 사용자가 로그인한 상태인지 확인할 수 있다.</p>
<blockquote>
<h4 id="spring-security-httpsession">Spring Security? HttpSession?</h4>
</blockquote>
<ul>
<li><strong>HttpSession</strong>: 로그인 상태를 서버가 기억하게 해주는 저장 공간</li>
<li><strong>Spring Security</strong>: 로그인, 로그아웃, 권한, 보안 처리를 도와주는 큰 보안 프레임워크
비밀번호 암호화, 접근 권한 설정, 관리자페이지 설정 등이 가능하다.</li>
</ul>
<p>이번 프로젝트에서는 로그인의 기본 흐름을 이해하는 것이 목적이었기 때문에 Spring Security를 사용하지 않고 HttpSession을 이용해 직접 세션 로그인 기능을 구현하였다.</p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/f4ff47b0-3ca8-453c-b064-6007e3daa867/image.png" alt=""></p>
<p>로그인 폼에서 입력한 이메일과 비밀번호를 Service로 보내 검증하고 로그인에 성공하면 HttpSession에 이메일과 이름을 저장하여 로그인 상태를 유지하도록 구현하였다.</p>
<h3 id="step6-로그아웃-기능-구현하기">STEP6. 로그아웃 기능 구현하기</h3>
<p>로그아웃은 세션을 삭제하는 방식을 사용했다. 
<code>session.invalidate()</code>를 호출하면 세션에 저장된 로그인 정보가 제거되므로 로그인 상태가 해제된다.</p>
<h1 id="3-느낀점-및-소감">3. 느낀점 및 소감</h1>
<p>오랜만에 하려니까 손가락이 영타치는 법을 다시 까먹은 것 같아요.. 제멋대로 움직이네여 손가락이.. 
저는 코딩레시피 2025 최신버전을 보고 과제를 했는데 최신영상이라 화면이 비슷해서 정말정말 편하고 좋았어요!!!!😍
강사님 화면이랑 바로 비교하고 어디가 잘못되었는지 확인이 가능하다는 게 너무 편했습니다.. 
프론트엔드를 예쁘게 만들고 싶은데 시간이 없어서 많이 알아보지도 못하고 코드 복사 붙여넣기만 해서 부끄럽네요 시간이 될때마다 조금씩 알아보고 수정하면서 벨로그에 업데이트 하도록 하겠습니다ㅎㅎ 시험기간이 한번 지나고 나니 이런 여유있는 시간이 너무 소중하게 느껴져요..
다들 시험보시느라 너무 수고하셨고 다시 열심히 해봐요!!🔥🔥</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[<멋쟁이사자처럼> 3주차]]></title>
            <link>https://velog.io/@j_tnqls311/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-3%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@j_tnqls311/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-3%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Thu, 02 Apr 2026 09:48:50 GMT</pubDate>
            <description><![CDATA[<h1 id="1-김영한의-spring-입문">1. 김영한의 Spring 입문</h1>
<p>*<em>Spring이란? *</em>
: 자바로 웹 서비스를 만들 때 사용하는 개발 도구(프레임워크)</p>
<p>Spring 없이 만들면:</p>
<p>객체 생성 직접 해야 함
코드가 복잡해짐
유지보수 어려움</p>
<p>지금은 Spring을 사용해도 어렵다고 느껴지지만 나중 가면 갈수록, 프로젝트가 커질수록 Spring을 사용하면 관리가 쉽다고 느껴지게 된다</p>
<h2 id="1-섹션2-프로그램-환경설정">1) 섹션2. 프로그램 환경설정</h2>
<blockquote>
<h3 id="문제점1-자바-17-설치-및-연동">문제점1. 자바 17 설치 및 연동</h3>
<p>처음에 자바 11로 잘못 다운받고 다시 자바 17을 오라클에서 다운받고 설치까지는 했는데.. 검색해보니까 시스템 환경변수 설정에 들어가서 경로설정도 해야한대서 하라는대로 했다고 생각했는데 index.html을 실행시키려고만 하면 404 NOT FOUND ERROR 문자가 떴다.. 
<img src="https://velog.velcdn.com/images/j_tnqls311/post/26c2a6ad-f2f9-40a0-9c43-76461b12b081/image.png" alt="">
<img src="https://velog.velcdn.com/images/j_tnqls311/post/896c061f-4d2a-405c-9ae4-adf583b865a9/image.png" alt="">
이틀동안 쳇 GPT와 씨름하며 해결하려고 했지만 나혼자 해결하지 못하고 지인님께 도움을 받았다(정말 압도적 감사합니다....)
핵심적인 문제는 <strong>자바는 17로 다시 다운 받았으면서 스프링은 자바 11 상태로 내버려두었던 것</strong>이 첫번째고 아래 사진처럼 <strong>SpringApplication을 통해 실행시켜야 했는데 그러지 다른 demo1이라는 것을 통해 실행</strong>시킨 것이 두번째인 것 같다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/6c054f35-3ca9-4995-9030-e1bd9e5c4820/image.png" alt=""></p>
<h2 id="2-섹션3-스프링-웹-개발-기초">2) 섹션3. 스프링 웹 개발 기초</h2>
<h3 id="1-정적-컨텐츠">1. 정적 컨텐츠</h3>
<p>인텔리제이의 html 기본 세팅에서 <body>와 </body> 사이에 작성한 말이 웹 페이지에 그대로 나타난다. 이를 정적컨텐츠를 제공한다고 한다. (가공되지 않은 파일 자체를 반환)
<img src="https://velog.velcdn.com/images/j_tnqls311/post/f9c1dc02-b872-4e5c-aee2-f1133635eb44/image.png" alt="">
사용자가 localhost:8080/hello-static.html 요청 -&gt; 톰캣이 요청 전달(Spring에 내장) -&gt; 톰캣이 컨트롤러를 찾음(동적처리) -&gt; 없으면 정적 파일 resources/static/hello-static.html 확인 -&gt; 파일 그대로 응답(코드실행X)</p>
<blockquote>
<h4 id="동적-컨텐츠">+동적 컨텐츠</h4>
<p>동적 컨텐츠란 정적 컨텐츠와 반대되는 것으로 <strong>사용자의 요청에 따라 내용이 바뀌는 앱 컨텐츠</strong>이다.</p>
</blockquote>
<h3 id="2-mvc와-템플릿-엔진">2. MVC와 템플릿 엔진</h3>
<p>MVC: 각 Model, View, Controller으로 탬플릿의 역할을 나눠서 프로그램을 만드는 구조라고 할 수 있다. (가공된 HTML 반환)
-&gt; 관심사를 분리하여 코드의 유지보수성을 높이기 위함
<strong>Controller</strong>: /hello가 입력되면 hello spring 페이지 보여주기
<img src="https://velog.velcdn.com/images/j_tnqls311/post/31bbae55-a4c8-41ae-bc03-1806d758acbc/image.png" alt="">
<strong>Model</strong>: Controller가 View에 넘겨줄 데이터
<img src="https://velog.velcdn.com/images/j_tnqls311/post/c14679d1-50f2-405e-b3cb-88620beaa220/image.png" alt="">
<strong>View</strong>: 사용자에게 보여지는 HTML
<img src="https://velog.velcdn.com/images/j_tnqls311/post/54c8606d-d747-4296-bcb4-d96d18d34507/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/f817e3dc-e94a-4b38-a20e-439bbad2fba0/image.png" alt=""></p>
<h3 id="3-api">3. API</h3>
<p>*<em>API란? *</em> view를 사용하지 않고 객체(데이터)를 반환하는 방식
-&gt; 프론트엔드와 분리하기 위해</p>
<p><strong>@ResponseBody</strong>: 화면(view)를 쓰지 않고 데이터로 바로 응답하라는 어노테이션
-&gt; return 값을 그대로 HTTP 응답 바디에 넣음</p>
<p>이때 객체 변환은 담당하는 것이 <strong>json</strong>이다. json 말고도 XML을 사용할 수 있지만 가볍고 빠른 장점을 가진 json을 표준적으로 사용한다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/0b5ac471-f6b9-4f6d-b0ca-08fd585f5b0b/image.png" alt=""></p>
<h2 id="3-섹션4-회원관리-예제---백엔드-개발">3) 섹션4. 회원관리 예제 - 백엔드 개발</h2>
<h3 id="1-test">1. Test</h3>
<p><strong>@AfterEach</strong>: 메모리 DB에 저장된 데이터 삭제(@AfterEach 만날 때마다 실행)
한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있음
-&gt; 이전 테스트 때문에 다음 테스트가 실패할 가능성이 생긴다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/10bddbd5-0c8a-49f1-bada-c64243936c44/image.png" alt=""></p>
<p><strong>Ctrl+shift+t</strong>: test 코드를 일일히 작성하지 않아도 class 이름 부분에 커서를 놓고 단축키를 실행시키면 테스트 파일이 만들어진다.</p>
<p><strong>//given //when //then</strong>: &#39;주어진 상황에서(given) 무언가를 실행했을 때(when) 나와야 하는 결과(then)&#39;로 주석을 나누어 확인하는 것
<img src="https://velog.velcdn.com/images/j_tnqls311/post/fac5a473-4734-4346-9272-573c55c792c5/image.png" alt=""></p>
<p><strong>@BeforeEach</strong>: 각 테스트 실행 전에 호출, 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고 의존관계를 맺어준다</p>
<h2 id="4-섹션5-스프링-빈과-의존관계">4) 섹션5. 스프링 빈과 의존관계</h2>
<p><strong>스프링 빈</strong>: 스프링이 만들어주고 관리해주는 객체
우리가 new를 통해서 객체를 만들지 않아도 됌</p>
<blockquote>
<p><strong>왜 써야할까?</strong></p>
</blockquote>
<ul>
<li>우리가 직접 객체 일일히 선언하면 관리가 복잡해짐</li>
<li>클래스 안에 객체를 선언하는 경우 둘의 의존관계가 지나치게 강해져 수정이 힘든 경우가 있음</li>
<li>의존성 주입(DI)을 쉽게 할 수 있음 -&gt; 코드가 깔끔해짐
1 @Component 붙은 클래스 발견
2 객체 만들어서 컨테이너에 저장
3 @Autowired 발견
4 필요한 타입 확인
5 같은 타입 객체 찾아서 넣어줌</li>
</ul>
<blockquote>
<p><strong>자바 코드로 직접 스프링 빈 등록(수동) vs 컴포넌트 스캔(자동)</strong></p>
</blockquote>
<pre><code>@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService() {
        return new MemberService();
    }
}</code></pre><p>명확하고 제어가 가능 BUT 코드가 복잡해짐</p>
<pre><code>@Component
public class MemberService {
}</code></pre><p>단순하고 간편함 BUT 어디서 등록되었는지 명확하지 않음</p>
<h2 id="5-섹션7-스프링-db-접근-기술">5) 섹션7. 스프링 DB 접근 기술</h2>
<blockquote>
<h3 id="문제점2">문제점2.</h3>
<p>H2 database가 설치가 안됌..
<img src="https://velog.velcdn.com/images/j_tnqls311/post/47b5fc7a-1259-4195-ad36-3cc65b2912c9/image.png" alt="">
여기서 또 시간을 보낼 수 없어서 여기서 강의 보는 것을 중단하고 CRUD 구현으로 넘어갔다(도와주세요..ㅜㅜ)</p>
</blockquote>
<h1 id="2-직접-crud-구현하기">2. 직접 CRUD 구현하기</h1>
<p>CRUD란 소프트웨어에서 데이터를 다룰 때 가장 기본이 되는 4가지 작업을 묶어서 부르는 용어로 각 철자는 아래를 의미한다.
C: create(생성) -&gt; 새로운 데이터 추가
R: Read(읽기) -&gt; 저장된 데이터 조회
D: Update(갱신) -&gt; 기존 데이터 수정
D: Delete(삭제) -&gt; 데이터 삭제</p>
<p>앞서 했던 회원관리 예제 만들기도 CRUD를 이용한 예제였다.
나는 CRUD를 이용해 상품 관리 시스템을 만들어 보았다.</p>
<h2 id="step1-프로젝트-생성">step1. 프로젝트 생성</h2>
<p>C: 상품 추가
R: 상품 조회
U: 상품 이름 수정
D: 상품 품절시 상품 삭제</p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/923709f6-60ca-4d5c-8b85-044c95319d3d/image.png" alt="">
새 프로젝트를 생성하여 프로그램을 만들어보았다.</p>
<h2 id="step2-구조-잡기">step2. 구조 잡기</h2>
<p>강의를 다 들었을 때는 Spring의 응답 구조가 모호하게 기억에 남았는데 직접 프로젝트를 생성하기 위해 구조를 생각하다보니 보다 명확하게 Spring의 응답 구조를 이해할 수 있었다. 
<strong>사용자 입력 -&gt; Controller -&gt; Service -&gt; Repository -&gt; 수정 완료 -&gt; 다시 목록으로 이동</strong></p>
<p>Product 클래스를 생성하여 프로젝트에 상품 번호와 이름, 품절여부가 존재한다는 틀을 만들어주었다.</p>
<pre><code>package hello.Hello_Spring.domain;

public class Product {

    private Long id; //상품번호
    private String name; //상품이름
    private boolean soldOut; //품절여부

    //기본 생성자
    public Product(){
    }

    public Product(Long id, String name, boolean soldOut){
        this.id = id;
        this.name = name;
        this.soldOut = soldOut;
    }

    //getter setter
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id; 
    }

    public String getName() {
        return name; 
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isSoldOut() {
        return soldOut;
    }

    public void setSoldOut(boolean soldOut) {
        this.soldOut = soldOut;
    }

}</code></pre><blockquote>
<h4 id="getter-setter">getter setter</h4>
<p>: getter는 값을 꺼내고 setter는 값을 넣을 수 있음
자바에서 변수는 보통 안전성을 위해 private으로 설정되어있다.
따라서 외부에서 변수에 직접 접근하는 것이 불가능한데 이때 getter setter를 통해 값을 불러오고 새로 입력할 수 있게 된다. <strong>Alt+Insert</strong> 단축키를 통해 getter setter를 쉽게 불러올 수 있고 전체 선택시 자동으로 코드가 입력되어 편하다
(강의 들을 때는 몰랐는데 일일히 입력 안해도 되니까 엄청 편리한것 같다!!)</p>
</blockquote>
<p>저장소 클래스 만들기:
repository 패키지 안에 ProductRepository라는 클래스를 선언하여 데이터를 저장하는 클래스를 만들었다.  </p>
<pre><code>package hello.Hello_Spring.repository;

import hello.Hello_Spring.domain.Product;
import org.springframework.stereotype.Repository;


import java.util.*;

@Repository //저장소 선언
public class ProductRepositoy {

    private final Map&lt;Long, Product&gt; store = new HashMap&lt;&gt;();    //Long-상품번호, Product-실제상품정보

    private Long sequence = 0L; //상품번호가 자동으로 하나씩 증가함

    //상품 저장
    public Product save(Product product){
        sequence++;
        product.setId(sequence);
        store.put(product.getId(),product);
        return product;
    }

    //전체 상품 조회
    public List&lt;Product&gt; findAll(){
        return new ArrayList&lt;&gt;(store.values());
    }

    //상품 1개 조회(id를 통해)
    public Product findById(Long id){
        return store.get(id);
    }

    //상품 수정
    public Product update(Long id, Product updatedProduct){
        Product findProduct = store.get(id);

        if (findProduct != null){
            findProduct.setName(updatedProduct.getName());
            findProduct.setSoldOut(updatedProduct.isSoldOut());
        }
        return findProduct;
    }

    //상품 삭제
    public void delete(Long id){
        store.remove(id);
    }

}</code></pre><p>Repository와 Controller를 연결하기 위해 ProductService 클래스를 만들었다.</p>
<pre><code>package hello.Hello_Spring.service;

import hello.Hello_Spring.domain.Product;
import hello.Hello_Spring.repository.ProductRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    public Product addProduct (Product product){
        return productRepository.save(product);
    }

    public List&lt;Product&gt; getALlProducts() {
        return productRepository.findAll();
    }

    public Product getProduct(Long id){
        return productRepository.findById(id);
    }

    public Product updateProduct(Long id, Product product){
        return productRepository.update(id, product);
    }

    public void deleteProduct(Long id){
        productRepository.delete(id);
    }
}</code></pre><p>상품정보를 입력받기 위해 controller 패키지에 ProductController 클래스를 생성하였다. </p>
<pre><code>{

    private final ProductService productService;

    public ProductController(ProductService productService){
    this.productService = productService;
    }

    //상품 목록화면
    @GetMapping
    public String list(Model model){
        List&lt;Product&gt; products = productService.getALlProducts();
        model.addAttribute(&quot;products&quot;, products);
        return &quot;productList&quot;;
    }

    //상품 등록화면
    @GetMapping(&quot;/new&quot;)
    public String createForm(){
        return &quot;productForm&quot;;
    }

    //상품 등록처리
    @PostMapping(&quot;/new&quot;)
    public String create(Product product){
        productService.addProduct(product);
        return &quot;redirect:/product&quot;;
    }

    //정보 삭제
    @PostMapping(&quot;/delete/{id}&quot;)
    public String delete(@PathVariable Long id) {
        productService.deleteProduct(id);
        return &quot;redirect:/products&quot;;
    }

    //상품 추가
    @GetMapping(&quot;/edit/{id}&quot;)
    public String editForm(@PathVariable Long id, Model model) {
        Product product = productService.getProduct(id);
        model.addAttribute(&quot;product&quot;, product);
        return &quot;productEdit&quot;;
    }

    //상품 수정
    @PostMapping(&quot;/edit/{id}&quot;)
    public String edit(@PathVariable Long id, Product product) {
        productService.updateProduct(id, product);
        return &quot;redirect:/products&quot;;
    }
}</code></pre><h2 id="step3-실행-및-테스트">step3. 실행 및 테스트</h2>
<p>링크를 입력하니 페이지로 잘 이동하였다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/c27e8128-8206-425f-82ed-c71ac889d66f/image.png" alt=""></p>
<blockquote>
<h3 id="문제점3">문제점3.</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/5fd602c4-2363-4de1-9308-b3df7883d635/image.png" alt="">
이름을 입력하고 등록 버튼을 누르니 저런 에러페이지가 떴다. 
또GPT에게 이게 무슨 에러냐고 물어보니  경로 이름이 맞지 않아서 생긴 에러라고 한다..
빨간 밑줄은 없었는데 어디가 잘못된 걸까?????</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/48e59c39-6d34-4f22-9a3b-ff44ddb06161/image.png" alt="">
목록 보기는 잘 작동하는 것을 확인할 수 있었고 상품 삭제 또한 정상적으로 기능하였다.
<strong>하지만 수정을 누르니 또 에러가 발생하였다...</strong>
상품 등록부분에 에러가 있는 것 같다</p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/53144829-e7e9-4be5-b7e0-ebf288510653/image.png" alt="">
품절을 누르면 사진처럼 품절 카테고리에 true라고 입력된다.</p>
<h1 id="3-느낀점-및-소감">3. 느낀점 및 소감</h1>
<p>ㅜㅜ자바 코딩을 하다가 막힌 것도 아니고 스프링 부트를 아예 실행조차 못해서 과제를 못할 뻔했다는게 정말 무서운 사실이다..ㅋㅋㅋㅋ
코딩하다 잘못된건 아직 간단한 코드여서 그런지 밑줄도 쳐주고 에러 메세지도 있어서 어디가 문제인지 알 수 있고 나머지는 쳇GPT랑 말해보면 되는데 이런 환경설정??은 어디서부터 어떻게 어디가 잘못된건지를 아직 잘 모르겠어서 어떻게 물어봐야 할지도 모르겠고 어디를 보여줘야 할지 막막해서 힘들다.
동아리 들어오길 정말 잘했다🥲🥲 코딩만렙사이쪼렙이되...</p>
<p>스프링 강의를 들으면서 자꾸 멍을 때리게 되는 느낌이다.
이제 슬슬 어렵고 이해가 안가기 시작해서인가 조금 정신을 놓으면 강사님은 저만치 가있고 나는 지금 저분이 뭘한거지? 상태ㅜㅜ 차라리 배속을 해서 들으면 강사님 말에 더 집중하게 되는 기분이다.
그만큼 여러번 되감기 해야 하긴 하지만
처음보다 강의시간 짧아져서 좋아했는데 내용은 호락호락하지 않네😂</p>
<p>이번에 Spring과 하찮지만 html을 구현해본 결과 절대 이건 한번 해가지고는 기억에 남지 않을 것 같다는 생각이 들었다.
매일매일 꾸준히 해야 손가락이 기억해서 할 수 있을 것 같은 느낌
지금은 코드 입력을 하면서도 이게 뭐지 이건 왜 쓰는 거지 하는 부분들이 있다.
일단 이번엔 시간이 없어서 그냥 치긴 했지만 이렇게 넘어가면 다음엔 더 모를 것 같다..ㅜㅜ
공부를 더 해야할 것 같다
아직 갈길이 멀구나... 힘내자!!!!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[<멋쟁이사자처럼> 2주차]]></title>
            <link>https://velog.io/@j_tnqls311/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-2%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@j_tnqls311/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-2%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Mon, 30 Mar 2026 04:47:34 GMT</pubDate>
            <description><![CDATA[<h1 id="1-객체지향이란">1. 객체지향이란?</h1>
<h4 id="객체지향이란-데이터속성와-기능메소드을-하나의-객체로-묶어-프로그램을-구성하는-프로그래밍-방식이다">객체지향이란 데이터(속성)와 기능(메소드)을 하나의 객체로 묶어 프로그램을 구성하는 프로그래밍 방식이다.</h4>
<h3 id="1-왜-객체지향을-사용해야-할까">1) 왜 객체지향을 사용해야 할까?</h3>
<h4 id="코드의-복잡성을-줄이고-재사용성을-높이기-위해">코드의 복잡성을 줄이고 재사용성을 높이기 위해</h4>
<p>강의에서 객체지향에 대해 옛날 컴퓨터로 예를 들어 설명해주시는 부분이 인상 깊었다. </p>
<blockquote>
<p>예전에는 컴퓨터의 본체, 모니터, 키보드가 하나로 단일화되어 있어 한 부분이 망가지면 새로 사야하거나 고치는데 돈이 많이 들었다.
현재는 모니터, 본체, 키보드를 분리하여 기능들을 부품화 시킨 덕분에 구성요소 중 하나가 고장났을시 교체가 용이해지게 되었다.
=&gt; 객체지향을 사용하는 이유</p>
</blockquote>
<p>하지만 기술이 경량화되며 컴퓨터는 작아지게 되었고 그 결과 부품화는 제거되고 있다. =&gt; 상황에 따라 부품화는 중요하지만 그보다 중요한 것은 적절한 사용</p>
<h3 id="2-메소드와-객체지향">2) 메소드와 객체지향</h3>
<blockquote>
</blockquote>
<h3 id="문제점1-메소드-vs-객체지향-무엇이-다를까">문제점1. 메소드 vs 객체지향 무엇이 다를까?</h3>
<p> 기능이 저장되어있는 메소드를 통해 코드의 양을 극적으로 줄일 수 있다.
BUT 프로그램이 커지면 많은 메소드들이 생기기 때문에 메소드와 변수를 관리하기 어려워짐 =&gt; 발전의 필요성
<strong>매소드의 집합 = 객체(프로그램 안의 작은 프로그램)</strong></p>
<h4 id="메소드는-하나의-프로젝트에서-밖에-못쓰지만-객체는-호출하여-여러-프로그램에-쓸-수-있다">메소드는 하나의 프로젝트에서 밖에 못쓰지만 객체는 호출하여 여러 프로그램에 쓸 수 있다.</h4>
<pre><code>class Calculator{
    int left, right;

    int void setOprands(int left, int right){
        this.left = left;
        this.right = right;
    }
    public void sum(){
        System.out.println(this.left+this.right);
    }
    public void avg(){
        System.out.println((this.left+this.right)/2);
    }
}
//Calculator의 설계도, 즉 class
//Calculator 안에 있는 setOprands는 메소드임</code></pre><blockquote>
<h4 id="thisleft와-left의-차이점">this.left와 left의 차이점</h4>
<p>this.left = Calculator class안에 있는 left 변수 전부
left = 우리가 left에 직접 입력한 값</p>
</blockquote>
<p>*은닉화(캡슐화): 작동원리가 어떻게 되어있는지를 인지하지 못하더라도 사용방법만을 통해 쓸 수 있는 것.
은닉화가 잘 되어있을수록 좋은 객체이다.</p>
<h3 id="3-클래스와-인스턴스">3) 클래스와 인스턴스</h3>
<p>클래스: 설계도
인스턴스: 만들어진 물건</p>
<blockquote>
<p>클래스 속 변수는 모든 인스턴스에 영향을 미침 
인스턴스의 변수는 각 인스턴스에서도 별개의 값을 가질 수 있음
<strong>모든 인스턴스가 공유하는 값
인스턴스에 따라 변하지 않는 값 클래스에 변수 지정
인스턴스를 생성할 필요가 없는 값
=&gt; 클래스에 변수 지정</strong></p>
</blockquote>
<pre><code>Calculator c1 = new Calculator(); 
//Calculator라는 객체 생성 후 c1이라는 변수에 담음
=&gt; 이러한 과정을 통틀어 인스턴스라고 함

c1.setOprands(10,20);
c1.sum();
c1.avg();</code></pre><p>객체는 상태와 행위의 집합이라고 할 수 있다.
Calculator 객체에서 left=10, right=20이라고 하자.
이 값이 상태이고 이 값을 통해 결과를 얻는 과정을 행위라고 한다.</p>
<blockquote>
<p><strong>인스턴스 메소드 -&gt; 클래스 맴버 접근 O
클래스 메소드 -&gt; 인스턴스 맴버 접근 X</strong>
클래스를 기반으로 인스턴스가 생성되는 것이기 때문에 선후관계로 인해 접근할 수 없다.</p>
</blockquote>
<h3 id="4-범위설정스코프">4) 범위설정(스코프)</h3>
<p>디렉토리로 설명한 부분이 인상깊었다. </p>
<blockquote>
</blockquote>
<p>파일을 저장할 때 이름이 겹치면 저장이 안된다.
숫자, 기호, 띄어쓰기 등을 동원하여 안겹치도록 하지만 불편함
=&gt; 디렉토리 도입을 통해 같은 디렉토리 안에서는 이름이 중복되면 안되지만 다른 디렉토리 안에 있으면 중복이 허용된다.
<strong>=&gt; 스코프 지정의 중요성</strong></p>
<h3 id="5-생성자">5) 생성자</h3>
<pre><code>class Calculator{
    int left, right;

    int void setOprands(int left, int right){
        this.left = left;
        this.right = right;
    }
    public void sum(){
        System.out.println(this.left+this.right);
    }
    public void avg(){
        System.out.println((this.left+this.right)/2);
    }
}
</code></pre><p>인스턴스를 만들고 클래스를 실행할 때 setOprands를 통해 값을 지정해줘야 한다는 불편함이 있음</p>
<pre><code>class Calculator{
    int left, right;

    **public Calculator(int left, int right){
        this.left = left;
        this.right = right;
    }**

    public void sum(){
        System.out.println(this.left+this.right);
    }
    public void avg(){
        System.out.println((this.left+this.right)/2);
    }
}</code></pre><p>위에 코드처럼 수정해주면 따로 setOprands를 호출하여 변수를 지정할 필요가 사라진다. 이를 <strong>생성자</strong>라고 한다.
생성자는 클래스가 생성될때 클래스와 똑같은 이름을 가진 메소드(생성자)를 제일 우선으로 실행되도록 한다. 이를 <strong>초기화 작업</strong>이라고 한다.</p>
<h3 id="6-상속⭐">6) 상속⭐</h3>
<h4 id="기존의-객체를-그대로-유지하면서-어떠한-기능을-추가하는-것">기존의 객체를 그대로 유지하면서 어떠한 기능을 추가하는 것</h4>
<p>객체지향을 통해 달성하고자 하는 제 1목표는 <strong>재활용성을 높이는 것</strong>이다.
상속은 객체지향의 재활용성을 극대화시키는 기법임.</p>
<blockquote>
<h4 id="상속은-왜-필요할까">상속은 왜 필요할까?</h4>
</blockquote>
<ol>
<li>객체를 내가 만들지 않아서 소스를 변경할 수 없을 때 사용</li>
<li>잘 사용되지 않는 기능을 추가할 때</li>
<li>여러 상황에서 사용되는 객체에 기능을 추가할 때(인스턴스의 기능과 충돌할 수 있으므로)</li>
</ol>
<p>기존의 객체는 기능을 물려준다는 의미에서 <strong>부모객체(상위클래스)</strong>가 되고,
새로운 객체는 기능을 물려받으므로 <strong>자식객체(하위클래스)</strong>가 된다.</p>
<pre><code>class SubstractionableCalculator extends Calculator{
    public void substruct(){
        System.out.println(this.left - this.right)
    }
}</code></pre><p>위 코드에서 SubstractionableCalculator는 extends를 통해 Calculator를 부모객체로 하는 자식객체가 된다.</p>
<h3 id="6-1-기본생성자-설정의-중요성">6-1) 기본생성자 설정의 중요성</h3>
<pre><code>class Calculator{
    int left, right;

    public Calculator(int left, int right){
        this.left = left;
        this.right = right;
    }

    public void sum(){
        System.out.println(this.left+this.right);
    }
    public void avg(){
        System.out.println((this.left+this.right)/2);
    }
}

class SubstractionableCalculator extends Calculator{
    public void substruct(){
        System.out.println(this.left - this.right)
    }
}</code></pre><p>위 코드에선 컴파일 오류가 뜰 것이다. 
<strong>why? Calculator 기본 생성자가 없기 때문</strong>
위 코드를 오류없이 실행시키고 싶다면 클래스에 <strong>public Calculator(){}</strong>, 즉 기본 생성자를 추가하여 초기화를 시켜야 한다. </p>
<blockquote>
<h3 id="문제점-3-super에-대한-이해">문제점 3. super에 대한 이해</h3>
<p>강의를 들을 때 super에 대한 이해가 잘 되지 않아 추가적으로 찾아보았다. 
<strong>super: 부모 생성자를 호출해주는 문법</strong></p>
</blockquote>
<pre><code>class Parent {
    Parent(int x) {
        System.out.println(&quot;부모&quot;);
    }
}
class Child extends Parent {
    Child() {
        super(10); // 부모 생성자 호출
        System.out.println(&quot;자식&quot;);
    }
}</code></pre><p>이때 super();를 통해 부모 생성자를 호출하지 않았다면 컴파일에러가 뜬다.
super를 사용하고 싶지 않다면 클래스에 기본 생성자를 추가해야 한다.</p>
<h3 id="6-2-overriding">6-2) overriding</h3>
<p>기존에는 부모 클래스의 기능에 자식 클래스를 이용하여 <strong>기능을 새롭게 추가</strong>하는 것에 대한 이야기를 했다. 
이번 overriding은 자식 클래스가 부모 클래스의 <strong>기본적인 동작방법을 변경</strong>하는 기능을 수행한다.</p>
<pre><code>class Calculator{
    int left, right;

    public Calculator(int left, int right){
        this.left = left;
        this.right = right;
    }

    public void sum(){
        System.out.println(this.left+this.right);
    }
    public void avg(){
        System.out.println((this.left+this.right)/2);
    }
}

class SubstractionableCalculator extends Calculator{
    public void sum(){
        System.out.println(&quot;실행 결과는 &quot; + (this.left + this.right) + &quot;입니다&quot;);
}
    public void substract(){
        System.out.println(this.left - this.right);
}</code></pre><p>위 아래 모두 sum이라는 메소드가 저장되어 있다.
이 코드를 실행해보면 자바는 부모클래스의 sum이 아닌 자식 클래스의 sum을 실행시킨다는 것을 알 수 있다.</p>
<blockquote>
<p>부모 클래스와 자식 클래스 모두 가지고 있는 메소드를 실행시킴
=&gt; 자식 클래스의 메소드를 더 우선 순위에 둔다.
<strong>BUT</strong>. 부모와 자식 메소드의 <strong>리턴 데이터 타입</strong>과 <strong>이름</strong>, <strong>매개변수의 개수, 데이터 타입, 순서</strong>가 일치해야 한다.</p>
</blockquote>
<h3 id="6-3-overloading">6-3) overloading</h3>
<p>같은 메서드 이름을 가지고 있더라도 매개변수의 데이터 개수와 타입, 순서가 바뀌면 충돌 X</p>
<p>다음은 예시이다.</p>
<pre><code>class Calculator{
    int left, right;
    int third = 0;

    public void setOprands(int left, int right){
    System.out.println(&quot;setOprands(int left, int right)&quot;);
        this.left = left;
        this.right = right;
    }

    **public void setOprands(int left, int right, int third){
    System.out.println(&quot;setOprands(int left, int right, int third)&quot;);
        this.left = left;
        this.right = right;
        this.third = third;
   ** }

    public void sum(){
        System.out.println(this.left+this.right+this.third);
    }
    public void avg(){
        System.out.println((this.left+this.right)/2);
    }
}</code></pre><p><strong>매소드 오버로딩은 매개변수를 기준으로 사용된다. 
=&gt; 매개변수가 같지만 리턴타입이 다르면 오류가 발생한다.</strong></p>
<blockquote>
<h3 id="문제점-3-overriding과-overloading의-헷갈림">문제점 3. overriding과 overloading의 헷갈림</h3>
<p>overriding과 overloading이 헷갈린다.
하지만 당장 이 이름을 외우는 것보다 코드를 써보며 기능을 익히는 것이 더 중요하다고 하셔서 간단하게만 정리해 보겠다. 
*<em>Overriding: 부모 클래스의 메서드를 자식 클래스에서 다시 정의하는 것
Overloading: 같은 이름의 메서드를 여러 개 만드는 것
*</em></p>
</blockquote>
<h3 id="7-클래스-패스">7) 클래스 패스</h3>
<p>컴퓨터의 저장장치 어딘가에 존재하는 클래스 파일을 사용하기 위한 수단</p>
<h3 id="8-패키지">8) 패키지</h3>
<p><strong>하나의 클래스 안에서 같은 이름의 클래스들을 사용하기 위한 수단</strong></p>
<p>intellij에서 패키지를 사용해보았기 때문에 익숙한 느낌이 든다.</p>
<p>어떻게 객체지향을 활용해서 부품화를 하고 복잡성을 줄일 수 있는 건가, 메소드와 뭐가 다른거지 하는 의문이 남아있었는데 경로와 패키지를 활용하여 클래스를 불러오는 것이구나를 깨달았다.
<del>더불어 intellij가 정말 편리한 것임을 알게되었다.</del></p>
<blockquote>
<h3 id="api">API</h3>
</blockquote>
<pre><code>System.out.println(1);</code></pre><p>위 코드에서 print(1)은 메소드, System.은 클래스, out은 변수이다. 
우리는 어떻게 불러오지도 않은 클래스 System을 쓸 수 있는 것일까?
=&gt; System은 java lang에 있는 클래스, 이 패키지에 있는 클래스들은 자바 코딩을 할 때 매우 필수적이고 자주 사용하고 중요하기 때문에 자동으로 import되어 사용할 수 있게 되어있다.</p>
<h3 id="9-접근제어자">9) 접근제어자</h3>
<pre><code>package control;
class A{
    public String y(){
        return &quot;public void y()&quot;;
    }
    private String z(){
        return &quot;public void z()&quot;;
    }
    public String x(){
        return z();
    }
}

public class Access {
    static void main(String[] args) {
        A a = new A();
        System.out.println(a.y());
        //아래 코드는 오류를 발생시킨다.
        //System.out.println(a.z());
        System.out.println(a.x());
    }
}</code></pre><p>위 코드에서 private로 묶인 z는 호출할 수 없다.
=&gt; 접근이 클래스 내부로 제한되어있기 때문
<strong>BUT.</strong> public인 x를 통해 우회하여 z를 쓸 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/c4982ff2-f9bc-45d2-8b7a-c86ea4c13446/image.png" alt=""></p>
<blockquote>
<h3 id="왜-접근제어자를-사용하는가">왜 접근제어자를 사용하는가?</h3>
</blockquote>
<ol>
<li>데이터를 보호하기 위해
모든 변수나 메서드를 아무데서나 접근할 수 있게 하면
값이 마음대로 바뀌어서 프로그램이 망가질 수 있음</li>
<li>역할을 나누기 위해
외부에서 사용하는 기능과 내부 기능을 구분하기 위함</li>
<li>코드 유지보수를 쉽게 하기 위해
접근 범위를 제한하면 코드 구조가 깔끔해지고 수정이 쉬워짐</li>
</ol>
<h3 id="9-1-클래스-접근제어자">9-1) 클래스 접근제어자</h3>
<p>위는 클래스 맴버에 관한 접근제어자였다. 이번엔 클래스의 접근제어자를 알아보자. 클래스의 접근제어자는 public과 default로 나뉜다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/903e73f0-7b2d-4932-992e-7eacfc972b59/image.png" alt=""></p>
<h3 id="10-abstract추상">10) abstract(추상)</h3>
<h4 id="상속을-강제하기-위한-것">상속을 강제하기 위한 것</h4>
<p>=&gt; 메소드의 핵심만 정의해놓고 그 메소드의 실제  동작은 하위 클래스에 위임하고 있음</p>
<blockquote>
<h3 id="왜-abstract를-사용하는가">왜 abstract를 사용하는가?</h3>
<p>공통적인 부분만 정의하고 상황에 따라 가변적이 될 수 있는 부분은 사용자에게 위임함. 주로 큰 프로젝트에서 사용되는 경우가 많음.
<strong>일회성 클래스를 여러개 만들기보다 공통된 부분만 클래스를 만들어 두는 것</strong></p>
</blockquote>
<blockquote>
<h3 id="final">final</h3>
<p>abstract와 반대되는 개념
field: 변경을 금지시킴
메소드, 클래스: 상속을 금지시킴</p>
</blockquote>
<h3 id="11-인터페이스">11) 인터페이스</h3>
<ol>
<li>하나의 클래스가 여러개의 인터페이스를 구현할 수 있다.</li>
<li>인터페이스도 상속된다.</li>
<li>인터페이스의 맴버는 public이어야 한다.</li>
</ol>
<blockquote>
<h3 id="abstract와-interface">abstract와 interface</h3>
<p>공통점: 자식클래스가 메소드를 추가해야함
차이점: 인터페이스는 클래스가 아닌 고유한 형태를 가지지만 abstract는 추상 클래스로 존재함, 인터페이스는 구체적인 로직이나 상태를 가지지 못하는 반면 추상 클래스로서 구체적인 메소드를 가질 수 있음.
<strong>abstract는 기능을 만들어놓고 추가하는 느낌이라면
interface는 기능의 규칙만 정해놓고 나머지 전부를 만드는 느낌</strong></p>
</blockquote>
<h1 id="2-pbl-2주차-과제">2. PBL 2주차 과제</h1>
<h3 id="1-step1">1) step1</h3>
<blockquote>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/80b20cc0-b730-4e72-8a93-518fc776fe2b/image.png" alt=""></p>
</blockquote>
<pre><code>package lion.Week2;

import java.util.Scanner;

public class step1 {
    static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println(&quot;아기사자의 이름을 입력해주세요!&quot;);
        String name = scanner.nextLine();

        System.out.println(&quot;전공을 입력해주세요!&quot;);
        String major = scanner.nextLine();

        System.out.println(&quot;기수를 입력해주세요!&quot;);
        int grade = scanner.nextInt();

        System.out.println(&quot;입력값 검증을 진행합니다.&quot;);

        if (name == null){
            System.out.println(&quot;이름은 비어있을 수 없습니다.&quot;);
            return;
        }
        else if (major == null){
            System.out.println(&quot;전공은 비어있을 수 없습니다.&quot;);
            return;
        }
        else if (grade &lt; 1){
            System.out.println(&quot;올바르지 않은 기수입니다.&quot;);
        }
        else
            System.out.println(&quot;입력값 검증에 성공하여 아기사자 객체 생성을 진행합니다.&quot;);
            System.out.println(&quot;아기사자 객체정보를 성공적으로 저장하였습니다!&quot;);
            System.out.println(&quot;아기사자 정보를 출력합니다.&quot;);
            System.out.println(&quot;이름: &quot; + name + &quot; / 전공: &quot; + major+ &quot; / 기수: &quot; + grade);
    }
}
</code></pre><p>문제를 제대로 안읽어서 오류가 떴다...</p>
<blockquote>
<h3 id="문제점4">문제점4.</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/7d84cfcb-69dd-41cc-aa55-9e7d20a3e479/image.png" alt="">
int형으로 받으면 빈값을 체크할 수 없음 =&gt; String으로 받고 나중에 변환</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/4e79615a-da2b-4604-a497-7ea32f202e92/image.png" alt="">
다시 문제 제대로 읽고 1보다 작으면 경고문구 뜨는 걸로 바꾸었다.</p>
<h3 id="2-step-2">2) step 2</h3>
<blockquote>
<h3 id="결과-1">결과</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/e9ddcfff-347a-4992-bb58-7997596547a7/image.png" alt="">
<img src="https://velog.velcdn.com/images/j_tnqls311/post/3f4cfe52-9420-42de-9180-4023bdd8e122/image.png" alt=""></p>
</blockquote>
<pre><code>package lion.Week2;

import java.util.Scanner;

public class step4 {
    static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println(&quot;아기사자 이름을 입력하세요: &quot;);
        String name = scanner.nextLine();

        System.out.println(&quot;아기사자 전공을 입력하세요: &quot;);
        String major = scanner.nextLine();

        System.out.println(&quot;아기사자 기수를 입력하세요: &quot;);
        int grade = scanner.nextInt();

        System.out.println(&quot;입력이 완료되었습니다. 아기사자 객체 상태를 확인합니다.&quot;);

        String result = Validation.validate(name, major, grade);
        if (result == &quot;success&quot;){
            Lion lion = new Lion(name, major, grade);

            lion.printInfo();
        }
        else{
            System.out.println(result);
        }
    }
}
</code></pre><blockquote>
<h3 id="문제점5">문제점5.</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/a07fe6ef-0659-4f00-bc46-ff68d6825024/image.png" alt="">
점점 헷갈리기 시작하면서 앞내용이 기억 안나기 시작했다...(세미콜론도 안쓰고 대소문자도 안맞고 기수도 String으로 받음)
한개씩 입력받을 거라 저런식으로 썼었는데
모든 매개변수의 개수, 타입이 같아서 오류가 생긴 거였다.
<strong>=&gt; 한번에 매개변수를 다 받는 생성자로 수정</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/9c9483b4-9e7f-43d7-9bad-2d8795cb979c/image.png" alt=""></p>
<h1 id="3-pbl-3주차-과제">3. PBL 3주차 과제</h1>
<blockquote>
<h3 id="결과-2">결과</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/61de5a4a-725c-43bc-a0e0-4f64cb9b016c/image.png" alt="">
<img src="https://velog.velcdn.com/images/j_tnqls311/post/4b8112fa-2cc2-4b16-96f2-66ecdd746cb1/image.png" alt=""></p>
</blockquote>
<pre><code>// 실행코드
package lion.Week3;

import java.util.Scanner;

public class step3 {
    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);

        System.out.println(&quot;============아기사자 정보입력============&quot;);
        System.out.println(&quot;이름: &quot;);
        String newName = scanner.nextLine();
        System.out.println(&quot;전공: &quot;);
        String newMajor = scanner.nextLine();
        System.out.println(&quot;기수: &quot;);
        int newGrade = scanner.nextInt();
        scanner.nextLine();
        System.out.println(&quot;파트(백엔드 / 프론트엔드 / 디자인 / 기획): &quot;);
        String newPart = scanner.nextLine();
        System.out.println(&quot;학번: &quot;);
        int newYear = scanner.nextInt();
        scanner.nextLine();

        System.out.println(&quot;============운영진 정보입력============&quot;);
        System.out.println(&quot;이름: &quot;);
        String name = scanner.nextLine();
        System.out.println(&quot;전공: &quot;);
        String major = scanner.nextLine();
        System.out.println(&quot;기수: &quot;);
        int grade = scanner.nextInt();
        scanner.nextLine(); //엔터지우기!!!!!!!!!!!
        System.out.println(&quot;파트(백엔드 / 프론트엔드 / 디자인 / 기획): &quot;);
        String part = scanner.nextLine();
        System.out.println(&quot;직책(대표 / 부대표 / 파트장 / 멘토): &quot;);
        String role = scanner.nextLine();

        Lion lion = new Lion(newName, newMajor, newGrade, newPart, newYear);
        Staff staff = new Staff(name, major, grade, part, role);

        System.out.println();
        System.out.println(&quot;============아기사자 정보============&quot;);
        lion.printInfo();

        System.out.println();
        System.out.println(&quot;============운영진 정보============&quot;);
        staff.printInfo();

    }
}</code></pre><pre><code>//부모 클래스
package lion.Week3;

abstract class Member {
    String name;
    String major;
    int grade;
    String part;

    public Member(String name, String major, int grade, String part) {
        this.name = name;
        this.major = major;
        this.grade = grade;
        this.part = part;
    }
}</code></pre><pre><code>//자식클래스(스태프)
package lion.Week3;

class Lion extends Member implements SubmitPolicy{
    int year;

    public Lion(String name, String major, int grade, String part, int year){
        super(name, major, grade, part);
        this.year = year;
    }

    public boolean canSubmit() {
        return true;
    }

    public void printInfo(){
        System.out.println(&quot;==========결과 출력===========&quot;);
        System.out.println(&quot;이름: &quot; + name);
        System.out.println(&quot;전공: &quot; + major);
        System.out.println(&quot;기수: &quot; + grade);
        System.out.println(&quot;파트: &quot; + part);
        System.out.println(&quot;학번: &quot; + year);
        System.out.println(&quot;과제제출 가능여부: &quot; + canSubmit());
    }
}</code></pre><pre><code>//자식 클래스(아기사자)
package lion.Week3;

class Staff extends Member implements SubmitPolicy{
    String role;

    public Staff(String name, String major, int grade, String part, String role){
        super(name, major, grade, part);
        this.role = role;
    }

    public boolean canSubmit() {
        return false;
    }

    public void printInfo(){
        System.out.println(&quot;==========결과 출력===========&quot;);
        System.out.println(&quot;이름: &quot; + name);
        System.out.println(&quot;전공: &quot; + major);
        System.out.println(&quot;기수: &quot; + grade);
        System.out.println(&quot;파트: &quot; + part);
        System.out.println(&quot;역할: &quot; + role);
        System.out.println(&quot;과제제출 가능여부: &quot; + canSubmit());
    }
}</code></pre><pre><code>//인터페이스
package lion.Week3;

public interface SubmitPolicy {
    boolean canSubmit();
}</code></pre><blockquote>
<h3 id="문제점6">문제점6.</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/2f613eaa-3f2c-4c2d-940f-e0b75569a45c/image.png" alt=""></p>
</blockquote>
<ol>
<li>변수이름을 똑같이 설정해서 오류가 발생함</li>
<li>인터페이스를 적용하는 방법이 익숙하지 않음(쳇GPT의 도움을 많이 받음...🥲)</li>
<li>nextInt() 뒤에 nextLine으로 엔터 없애야 하는 거 또 까먹음</li>
</ol>
<h1 id="4-배운점과-느낀점">4. 배운점과 느낀점</h1>
<p>이번주 과제를 하며 제일 크게 느낀 것은 벨로그 보기 좋게 만드는게 제일 어려운 것 같다는 점이다. 
내용이 많아지니 체계적으로 보기 좋게 정리하는 것이 어렵고 쓰다보니 나조차도 점점 이걸 어디다 적어야 하는지 헷갈리기 시작했다.
그래도 미래의 내가 볼 거라고 생각하고 열심히 정리했다.</p>
<p>또한 overriding과 overloading, protected와 default처럼 이름과 기능이 헷갈리는 것들이 조금 있었는데 기가 막히게 그런 부분에서 강사님이 걱정할 필요가 없다고 해주셔서 마음이 놓였다.</p>
<p>나는 의외로 클래스 패스와 패키지 부분이 제일 힘들다고 느꼈는데 intellij를 쓸때에는 신경도 안썼던 부분에서 시간을 들여 공부를 한다고 느껴지니 더 집중을 못했던 것 같다.
그건 인텔리제이가 친절한 부분이지 나중에 자바 쓰게되면 다 필요한 것들인데....
인텔리제이에서는 간단하게 패키지를 만들고 경로를 설정할 수 있어서 이렇게 공부해야 하는 부분인지 몰랐다. 나중에 공동작업을 하게되거나 할때 경로에 대해 다시 더 공부해야 할 것 같다고 느꼈다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[<멋쟁이사자처럼> 1주차]]></title>
            <link>https://velog.io/@j_tnqls311/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-1%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@j_tnqls311/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-1%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 22 Mar 2026 18:38:51 GMT</pubDate>
            <description><![CDATA[<h1 id="1-김영한의-자바-입문">1. 김영한의 자바 입문</h1>
<h3 id="1-소감">(1) 소감</h3>
<p>김영한의 자바 입문을 통해 처음으로 자바를 시작하게 되었다.
개발자가 되려면 필수적으로 해야하는 언어라고 듣기도 했고 주변에 할줄 아는 사람들이 너무 많아서 뒤쳐지지 않기 위해서라도 열심히 해보자고 다짐하고 시작했다. 
강의를 들으며 코드를 치다보니 부끄럽지만 아직도 내가 영타가 이렇게 부족했나하는 생각이 들었다..
그래도 많이 발전했다고 생각했는데 아직 많이 느리고 모자란 것 같다.
자바는 작년에 조금씩 했던 자바 스크립트와는 비슷한 듯 다른 부분이 있어 신기했다. 개인적으로 왜인지는 모르겠지만 자바가 조금 더 어려운 느낌이 든다.
앞으로 더욱 열심히 공부해야겠다는 다짐을 할 수 있었던 시간이었다.
밑으로는 강의를 들으며 배운점과 시행착오 등을 적었다.</p>
<h3 id="2-섹션2-hello-world">(2) 섹션2. Hello World</h3>
<p>섹션 2에서는 기본적인 단축키와 프로그램 실행 방법에 대해 배웠다.
그 중에서도 psvm이라는 단축키가 있었는데 psvm을 입력하고 엔터를 누르면 static void main(String[] args) {}가 자동으로 입력된다. </p>
<blockquote>
<p>psvm -&gt; static void main(String[] args) {}</p>
</blockquote>
<p>여태까지 배웠던 파이썬이나 자바 스크립트 같은 경우에는 위와 같은 것을 입력하지 않아도 위에서부터 자동으로 코드가 실행되었는데 자바는 psvm을 입력하고 시작해야 한다는 점에서 차이를 느꼈다.</p>
<p>psvm을 입력하면 자동으로 static void main(String[] args) {}가 나와야 하는데 내가 설정을 잘못했는지 static void main() {} 이런 코드가 나와서 당황했다.</p>
<blockquote>
<p>문제점 1.
psvm -&gt; static void main() {} (x)
=&gt; file -&gt; setting -&gt; Live templates -&gt; psvm -&gt; Expand with Tab으로
설정변경</p>
</blockquote>
<p>static void main() {}로 두고 코드를 써도 잘 작동되길래 그냥 둬도 되나 하는 생각에 쳇 gpt에게 물어보았다.</p>
<p>🧠 왜 static void main()인데도 실행됐냐</p>
<p>IntelliJ는 똑똑해서 👇</p>
<p>👉 “main 비슷한 거 있네?”
👉 “그냥 실행해줄게~”</p>
<p>이렇게 자동으로 실행 진입점 만들어줌</p>
<p>✔️ 즉
static void main() {
}</p>
<p>👉 ❌ 정식 자바 규칙 아님
👉 ✔️ IDE가 봐준 것</p>
<p>위와 같은 답변을 들을 수 있었다.
답변을 들으니 앞으로를 위해서 이 문제를 해결해야겠다는 생각이 들었다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/de93df86-2664-4bd7-872a-0683097b461f/image.png" alt="">
file -&gt; setting -&gt; Live templates -&gt; psvm -&gt; Expand with Tab
위처럼 설정을 변경하여 해결할 수 있었다.</p>
<h3 id="3-섹션6-반복문">(3) 섹션6. 반복문</h3>
<p>섹션 6에서는 프로그램 개발시 코드작성을 줄이기 위해 필수적인 반복문을 배웠다.</p>
<blockquote>
<p>문제점 2.
나에게는 굉장히 안좋은 습관이 하나 있는데 하나를 습득해서 그게 익숙해지면 다른 효율적인 것이 나타나도 기존의 것을 고수하는 것이다.</p>
</blockquote>
<p>비효율적이더라도 조금 더 움직이는 것이 새로운 걸 배우고 익숙해지는 것보다 쉽다는 생각을 하는 것 같다.
하지만 장기적으로 보면 버리게 되는 시간은 비효율적인 기존의 방식을 고집했을 때가 훨씬 클 것이다.</p>
<p>이번에도 while문을 배우고 for문을 배우려고 보니 어차피 수행하는 역할은 똑같은데 그냥 건너뛰고 다음에 들을까 하는 생각이 들었다.</p>
<p>시시각각 트렌드가 변화하고 발전하는 지금 특히 개발자를 희망하는 사람이 이런 습관을 가지고 있는 것은 내가 생각했을 때 매우 좋지 않다..
이 생각이 들자마자 지금이라도 이런 습관을 빨리 없애버리고 싶어서 다른 섹션보다 이 섹션을 훨씬 공을 들여서 공부했던 것 같다.</p>
<h3 id="while-vs-for">while vs for</h3>
<p>앞서 말했듯 둘은 수행하는 역할에 있어서는 차이가 거의 없다.
섹션6 초반에는 위에서 말했듯 while이 비효율적인 기존의 것이고 for가 발전된 형태인줄 알았는데 배우고 보니 그건 아니라는 것을 알게 되었다.
둘다 개발시 많이 사용되는 코드이고 각자 맞는 상황이 있다.</p>
<blockquote>
<h4 id="while조건">while(조건){}</h4>
</blockquote>
<h4 id="for초기식-조건-증감식">for(초기식; 조건; 증감식){}</h4>
<p>for문은 한번에 초기식과 조건, 증감식을 모두 볼 수 있어 가독성이 좋다는 장점이 있다.
더불어 for문 안에서만 초기식이 사용되기 때문에 변수를 신경쓰지 않아도 된다.
반면 while문은 while문 바깥에 선언해야 하기 때문에 scope가 넓어지고 메모리 사용이 커진다.
만약 반복문 안에서만 변수가 사용된다면 메모리를 과도하게 크게 사용하는 것이 된다.
하지만 반복횟수가 명확하지 않고 변화가 다양하다면 while문이 더 적절하다.</p>
<blockquote>
<p>반복횟수가 정해짐 -&gt; for
반복문 안에서만 초기식이 사용됌 -&gt; for
변화가 다양함 -&gt; while</p>
</blockquote>
<h1 id="2-아기사자-명단-만들기">2. 아기사자 명단 만들기</h1>
<h3 id="1-만드는-과정">(1) 만드는 과정</h3>
<p>아래는 최종적으로 만든 프로그램이다.</p>
<pre><code>package lion;

import java.util.Scanner;

public class Week1 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        while(true){
            System.out.print(&quot;저장할 아기사자의 수를 5이상 입력하세요: &quot;);
            int number = scanner.nextInt();
            scanner.nextLine(); // 버퍼 비우기

            if (number &lt; 5){
                System.out.println(&quot;[오류!] 올바르지 않은 숫자입니다.&quot;);
                continue;
            }

            String[] names = new String[number];

            // 이름 입력
            for(int i = 0; i &lt; number; i++){
                System.out.print((i+1) + &quot;번째 아기사자의 이름을 입력해주세요: &quot;);
                names[i] = scanner.nextLine();
            }

            // 출력
            System.out.println(&quot;\n최종 출력된 아기사자 명단입니다.&quot;);
            for(int i = 0; i &lt; number; i++){
                System.out.println((i+1) + &quot;. &quot; + names[i]);
            }

            break; // 한번 실행 후 종료
        }

        scanner.close();
    }
}</code></pre><ol>
<li><p>사람 수를 입력받기
값을 입력받기 위해 Scanner를 사용하여 int number에 값을 저장했다.
이때 사람 수를 5보다 작게 설정할 경우 재입력을 받기 위해
if문을 사용하여 조건을 (number&lt;5)로 설정하였다.</p>
</li>
<li><p>이름을 사람 수만큼 입력받기
숫자가 5이상 입력되었다면 이름을 사람 수 만큼 입력받기 위해 for문을 사용하였다. while을 사용해도 되지만 강의에서 수업을 들었듯 코드를 간결하고 보기 쉽게 만들기 위해 for문을 사용했다.</p>
<blockquote>
<p>문제점 3. 
사람 수를 적고 엔터를 누르니 첫번째 이름 입력이 스킵되었다.
<img src="https://velog.velcdn.com/images/j_tnqls311/post/25362846-86ac-4817-8180-a2523d512875/image.png" alt="">
=&gt; nextInt(); 다음줄에 nextLine();을 사용하여 엔터를 비운다</p>
</blockquote>
</li>
</ol>
<p>오류를 보자 반복문 강의에서 </p>
<pre><code>int number = scanner.nextInt();
String name = scanner.nextLine();</code></pre><p>실행시 scanner.nextInt();가 숫자만을 가져가고 엔터는 그대로 남긴다고 하는 것이 떠올랐다.
그래서 다음 실행인 scanner.nextLine으로 넘어가면 엔터값이 남아있어 그대로 첫 번째 입력을 건너뛰게 된다.</p>
<pre><code>int number = scanner.nextInt();
            scanner.nextLine(); // 버퍼 비우기</code></pre><p>이를 방지하기 위해 nextInt(); 다음 줄에 nextLine();을 사용하여 엔터값을 없애주어야 한다.</p>
<ol start="3">
<li>명단 출력하기
입력받은 모든 이름을 확인하기 위해 for문을 사용하여 배열에 저장된 데이터를 순서대로 출력하였다.
각 이름 앞에는 번호를 붙여 (i+1) 형태로 출력함으로써 보다 가독성있는 형태의 명단을 제공하였다.</li>
</ol>
<h3 id="2-결과">(2) 결과</h3>
<p><img src="https://velog.velcdn.com/images/j_tnqls311/post/bad2c5e4-4980-4696-a164-3885df1c9900/image.png" alt=""></p>
<h3 id="3-소감">(3) 소감</h3>
<p>바로 최근에 들었던 강의를 토대로 프로그램을 만들어보니 직접 문제를 풀고 있다는 성취감을 얻을 수 있어서 좋았다.
또한 복습이 되어 더욱 확실히 머릿속에 기억될 것 같다는 느낌이 든다.
열심히 공부하다보니 점점 재미를 붙이게 되는 것 같아 다행이라는 생각이 든다. 
혼자 학원을 다닐 때보다 같이 일주일에 한번 있는 스터디를 위해 공부를 하는 지금이 훨씬 재미있고 발전하는 기분이다.
앞으로 있을 과제도 지금처럼만 열심히 하자!!!!!!</p>
<p>(과제인데 너무 일기형식으로 적은 것은 아닐까 걱정이 조금 된다ㅜㅜ)</p>
]]></description>
        </item>
    </channel>
</rss>