이번 프로젝트는 아이폰 크기로 진행해볼 예정이라 아이폰 사이즈로 와이어 프레임을 구성 해 보았습니다.

 

메인 페이지, 지출 수입 추가 페이지

로그인 및 회원 가입 페이지

지출, 수입 내역 관련 페이지

소식 공유 관련 페이지

회원 마이 페이지 

 

이렇게 총 19개 정도의 페이지를 구성 해봤습니다.

나중에 프로젝트를 진행하다 보면 추가, 수정, 삭제가 될 수도 있지만 현재는 이런 틀을 잡고 진행할 예정 입니다.

해당 프로젝트를 시작하려는 이유

해당 프로젝트를 진행하는 이유는 국비 기간동안 프로젝트를 혼자서 완성할 수 있는 수준 까지는 만들어 주었지만

spring security 부분을 배우지 못한 갈증으로 인해 프로젝트를 진행 하게 되었습니다.

그래서 아래 이미지에도 동일한 내용이 들어 가겠지만 해당 프로젝트에 집중하는 요소는 spring security 와 jwt, OAuth2 를 이용한 

소셜 로그인 적용, 소셜 로그인 로직과 일반 로그인 로직 분기를 어떻게 나누어야 할지 고민해가며 프로젝트를 진행할 예정 입니다.

 

해당 프로젝트를 사용하기 위해 

 

코드로 배우는 React with 스프링부트 API서버 강의 | 구멍가게코딩단 - 인프런

구멍가게코딩단 | 스프링 부트(Spring Boot ver3.1(3.2 호환))로 제작되는 API 서버와 리액트의 연동 프로젝트 완성하기! 포트폴리오 작성 부트캠프 과정 전체를 강의로 제작, '구슬이 서 말이어도 꿰어

www.inflearn.com

구멍가게 코딩단 강의

 

 

스프링 시큐리티

스프링 시큐리티 : Spring Security

www.youtube.com

개발자 유미님의 스프링 시큐리티, jwt, OAuth2 강의 들을 듣고 진행하게 되었습니다.

 

프로젝트 가 계획안 및 깃 주소

 

프론트 주소 

 

GitHub - Garbi93/selfprj2-front: 개인 프로젝트 알뜰가계 [프론트] 입니다. (진행중...)

개인 프로젝트 알뜰가계 [프론트] 입니다. (진행중...). Contribute to Garbi93/selfprj2-front development by creating an account on GitHub.

github.com

 

백엔드 주소

 

GitHub - Garbi93/selfprj2-back: 개인 프로젝트 알뜰가계[백엔드] 입니다. (진행중...)

개인 프로젝트 알뜰가계[백엔드] 입니다. (진행중...). Contribute to Garbi93/selfprj2-back development by creating an account on GitHub.

github.com

 

java.lang 패키지 소개

  • 자바가 기본으로 제공하는 라이브러리 (클래스 모음) 중 가장 기본인 되는 것
  • lang 은 language 의 줄임 말이다.
  • 모든 java 프로젝트 에서 기본 제공 되기 때문에 자동 import 된다.

java.lang 패키지의 대표 클래스

  • Object : 모든 자바 객체의 부모 클래스
  • String : 문자열
  • Integer, Long, Double : 래퍼타입 → 기본형 데이터 타입을 객체로 만든 것
  • Class : 클래스 메타 정보
  • System : 시스템과 관련된 기본 기능들 제공

Object 클래스

  • 자바에서 모든 클래스의 최상위 부모 클래스는 항상 Object 클래스 이다.
package lang.object;

// 부모가 없으면 묵시적으로 Object 클래스를 상속 받는다.
public class Parent {
    public void parentMethod() {
        System.out.println("Parent.parentMethod");
    }
}
package lang.object;

// Parent 를 명시적으로 상속 받늗다.
public class Child extends Parent{
    public void childMethod() {
        System.out.println("Child.childMethod");
    }
}

⇒ 다음과 같은 상속 구조를 갖게 된다.

⇒ 모든 자바의 최종 부모는 Object 가 되게 된다.

package lang.object;

public class ObjectMain {
    public static void main(String[] args) {
        Child child = new Child();
        child.childMethod();
        child.parentMethod();

        // toString 은 Object 클래스의 메서드 => Object 로 부터 상속 받아 쓰게 된다.
        String string = child.toString();
        System.out.println(string);
    }
}

⇒ Parent 는 Objcet 를 묵시적으로 상속 받았기 때문에 child 클래스 내에서도 Object 의 기능인 toString 을 사용 할 수 있는것 이다.

 

 

자바에서 Object 클래스가 최상위 부모 클래스인 이유??

  • 공통 기능 구현
    • 모든 객체에서 필요한 공통 기본 기능을 개발자가 직접 메서드를 만드는 것은 상당히 번거롭고 개발자 마다 메서드의 기능, 명 이 일관 되지 않기 때문에 공통 기능 구현 및 통일성을 위해 최상위 클래스로 사용되게 된다. → 프로그래밍의 단순화, 일관성의 목적
    • Object 제공하는 기능의 예
      • 객체 정보 제공 → toString()
      • 객체의 같음 을 비교 → equals() 등등의 기능이 있다.
  • 다형성의 기본 구현
    • Object 는 모든 class 들의 부모 class 타입 이다. → 따라서 어떤 타입으로 값을 받아와도 Object 타입으로 받아 올 수 있다.

Object 다형성

  • Object 는 모든 클래스의 부모 클래스 이므로 → 모든 객체를 참조 할 수 있다 .

Object dog = new Dog();
Object car = new Car();

⇒ Object 는 모든 객체의 부모 타입이므로 어떤 객체든 담을 수 있다.

  1. Object 타입의 Car class
package lang.object.poly;

public class Car {
    public void move() {
        System.out.println("자동차 이동");
    }
}

  1. Object 타입의 Dog class
package lang.object.poly;

public class Dog {
    public void sound() {
        System.out.println("멍멍");
    }
}

  1. Car 타입과 Dog 타입 class 를 사용 하는 클래스에서 Object 타입의 메서드 사용
package lang.object.poly;

public class ObjectPolyExample1 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Car car = new Car();

        action(dog);
        action(car);

    }

    private static void action(Object obj) {
        // obj.sound(); // 컴파일 오류 Object 는 sound() 기능이 없다.
        // obj.move(); // 컴파일 오류 , Object sms move() 가 없다.

        // 객체에 맞는 다운캐스팅 으로 호출 가능
        if (obj instanceof Dog dog) {
            dog.sound();
        } else if (obj instanceof Car car) {
            car.move();
        }
    }
}

⇒ 결론 Object 는 자식을 모두 담을 수 있지만 자식 내에 무었이 들어 있는지 자세히 알 순 없다.

⇒ 그래서 다운 캐스팅을 사용해 준다.


Object 배열

  • Object 는 모든 타입의 객체를 담을수 있고 Object 타입의 배열을 만들면 세상의 모든 객체를 담을 수 있는 배열이 된다.

Object - toString() 메서드

  • toString 메서드는 객체에 저장된 정보를 문자열 형태로 반환 해주는 기능을 말한다. → 디버깅과 로깅때 유용하게 사용 된다.

toString 오버라이딩

  • 기본적으로 toString 메서드를 사용해 나온 객체 값은 사용하기 쉽지 않아 오버라이딩 하여 사용한다.

 

메서드 오버라이딩하여 toString을 사용할 수 있다 Dog class 만 오버라이딩 하여 사용해보면

package lang.object.tostring;

public class Dog {
    private String dogName;

    private int age;

    public Dog(String dogName, int age) {
        this.dogName = dogName;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "dogName='" + dogName + '\\'' +
                ", age=" + age +
                '}';
    }
}

⇒ Dog class

package lang.object.tostring;

public class Car {
    private String carName;

    public Car(String carName) {
        this.carName = carName;
    }
}

⇒ Car class

package lang.object.tostring;

public class ToStringManin2 {
    public static void main(String[] args) {
        Car car = new Car("Model Y");
        Dog dog1 = new Dog("멍멍이1", 2);
        Dog dog2 = new Dog("멍멍이2", 5);

        System.out.println("1. 단순 toString 호출");
        System.out.println(car.toString());
        System.out.println(dog1.toString());
        System.out.println(dog2.toString());

        System.out.println("2. println 내부에서 toString 호출");
        System.out.println(car);
        System.out.println(dog1);
        System.out.println(dog2);
    }
}

⇒ Dog 와 Car 를 사용하면

1. 단순 toString 호출
lang.object.tostring.Car@7a81197d
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}
2. println 내부에서 toString 호출
lang.object.tostring.Car@7a81197d
Dog{dogName='멍멍이1', age=2}
Dog{dogName='멍멍이2', age=5}

⇒ 메서드 오버라이딩 한것은 한것대로 결과가 나오고 println에서도 오버라이딩 한것을 호출 한다.

  • 정리
    • Car 인스턴스는 오버라이딩 하지 않아서 기본 메서드 사용
    • Dog 객체는 오버라이딩을 한 덕분에 객체상태를 명확하게 제공

오버라이딩한 객체의 참조값을 찾고 싶다면??

String refValue = Integer.toHexString(System.identityHashCode(dog1));
System.out.println("refValue = " + refValue);

⇒ 다음과 같이 사용하면

refValue = 8efb846

⇒ 우리가 일반적으로보는 16진수의 참조값을 볼 수 있다.


Object와 OCP

public class ObjectPrinter {
    public static void print(Object obj) {
        String string = "객체 정보 출력: " + obj.toString();
        System.out.println(string);
    }
}

⇒ ObjectPrinter 라는 class 는 Object class에 의존 한다고 표현 하는데 Object class 는 구체적인 Dog나 Car 같은 우리가 생성한 구체적 class 를 의존하는것이 아니기 때문에 추상적인것을 의존한다 라고 표현한다.

 

ObjectPrinter class의 특징 살펴보기

  • 다형적 참조 - print(Object obj) 타입의 매개변수를 사용하여 다형적 참조를 가능하게 한다 → Dog, Car 객체등 모든 객체 받을 수 있게
  • 메서드 오버라이딩 - Object 클래스는 모든 객체의 부모 이기때문에 Object가 갖고 있는 모든 메서드 (toString()) 과 같은 것을 모든 클래스에서 메서드 오버라이딩 할 수 있다.

OCP 원칙

  • Open : 새로운 클래스를 추가, toString() 을 오버라이딩해 기능 확장 가능
  • Closed : 새로운 클래스가 추가되도 ObjectPrinter는 변경하지 않고 그대로 사용 가능하다.

정리

  • 지금까지 배운 내용은 System.out.println() 의 기능을 살펴본 것이다.
  • 자바 언어가 기본 제공하는 다양한 메서드들은 개발자가 필요에 따라 오버라이딩 해서 사용할 수 있도록 설계되어 있다.

의존관계?

  • 정적 의존관계 : 주로 class 간의 관계를 의미하고, class 관계 그림이 정적 의존 관계이다, 프로그램을 실행시키지 않고 class 내에 사용하는 타입들만 보고 쉽게 의존관계를 파악할 수 있다.
  • 동적 의존관계 : 프로그램을 실행하는 런타임 에서 확인할 수 있는 관계, Object 타입으로 모든 것을 받는 경우 프로그램을 실행 시켜 Car 타입인지 Dog 타입인지 알 수 있는 관계를 말한다.

⇒ 단순히 의존관계 또는 어디에 의존한다고 하면 주로 정적 의존 관계를 말한다.


equals() - 1. 동일성과 동등성

  • 동등성 : Object 에서 제공하는 동등성 비교를 위한 equals() 메서드 이다. → 두 객체가 논리적으로 동등한지 확인
  • 동일성 : == 연산자를 사용하여 두 객체의 참조가 동일한 객체를 가리키는지 확인 → 완전히 같은지 확인
  • equals 실행 순서 예시
user1.equals(user2)
return (user1 == user2) // Object.equals 메서드 안
return (x001 == x002) // Object.equals 메서드 안
return false
false

⇒ equals 메서드는 위와 같이 작동한다. 그러므로 특정한 값의 동등성 비교를 사용하고 싶으면 equals 메서드를 재정의 해야 한다


equals() - 2. 구현

package lang.object.equals;

public class UserV2 {
    private String id;

    public UserV2(String id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object obj) {
        // 인자로 넘어온 id 를 변수에 담기 Object 에는 id가 없어서 다운 캐스팅
        UserV2 user = (UserV2) obj;
        // 내 id와 넘어온 id 같은지 비교 하기
        return id.equals(user.id);
    }
}

⇒ equals 메서드를 오버라이딩 하여 비교할 변수 를 지정해 변수끼리 비교 하게 만들기

package lang.object.equals;

public class EqualsMainV2 {
    public static void main(String[] args) {
        UserV2 user1 = new UserV2("id-100");
        UserV2 user2 = new UserV2("id-100");

        System.out.println("identity = " + (user1 == user2));
        System.out.println("equality = " + user1.equals(user2));
    }
}

⇒ 직접 사용하는 코드에 객체를 넣어 비교 해보면

identity = false
equality = true

⇒ 동등성 비교에서 true를 리턴 하게 된다.

  • 객체의 참조가 달라 동일성이 다르다
  • user1 의 id와 user2 의 id 의 값은 동등하다.

⇒ 해당기능은 최소한의 버전이고 정확한 equals 를 만드려면 복잡하다.

인텔리 제이에서 제공하는 기능으로 만들어 줄 수 있다.

eqauls (동등성)오버라이딩 코드 만들기

  • 1. equals and hashCode 를 클릭해주고

  • 2. next 버튼 클릭

  • 3. 비교할 변수1 클릭 후 next

  • 4. 비교할 변수2 클릭 후 next

  • 5. 결과보기
@Override
    public boolean equals(Object object) {
        if (this == object) return true;
        if (object == null || getClass() != object.getClass()) return false;
        UserV2 userV2 = (UserV2) object;
        return Objects.equals(id, userV2.id);
    }

⇒ 그럼 동등성 비교 코드를 만들어 준다.

정리

  • 무조건 동일성 비교를 만들필요 없이 필요한 경우에만 재정의 하면 된다.
  • equals 와 hashcode는 함깨 사용되는데 컬렉션 프레임 워크 사용시 자세하게 알아보자

Object 정리

Object 의 나머지 메서드

  • clone( ) → 객체를 복사할때 사용, 잘 사용하지 않음
  • hashCode( ) → equals 와 종종 사용되고 컬렉션 프레임 워크 에서 자세히 다룰 예정
  • getClass( ) → 추후 class 강의에서 다룰 예정
  • notify( ), notifyAll( ), wait( ) → 멀티 쓰레드용 메서드 , 멀티쓰레드에서 자세하게 다룰 예정

Role Hierarchy (계층 권한)

  • 권한 A, B, C 가 있고 이중 권한이 A < B < C 로 진행 하고 싶은경우 계층권한을 사용하면 된다.

(사용방법 으로는 SecurityConfig 파일 내부에 🔽)

 // 계층 권한 설정
  @Bean
  public RoleHierarchy roleHierarchy() {
      RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();

      hierarchy.setHierarchy("ROLE_C > ROLE_B > ROLE_A"); // 권한 설정 순위 셋팅 

      return hierarchy;
  }

⇒ 해당 코드를 추가 해주고

// 이제 내부에 어떤 경로에 요청이 왔을때에 어떤 응답을 해줄지 설정 해주면 된다.
// 작성방식은 람다식으로 해주면 된다.
http.authorizeHttpRequests((auth) -> auth
        .requestMatchers("/","/login").permitAll()
        .requestMatchers("/my/**").hasAnyRole("A") // <- A,B,C 모두 들어감
        .requestMatchers("/admin").hasAnyRole("C") // <- C만 들어갈 수 있음
        .anyRequest().authenticated()
);

⇒ SecurityFilterChain 부분에 위와 같은 코드로 사용 가능하다.


해당 정리 내용은 유튜버 개발자 유미님 영상을 보고 정리한 내용 입니다.

좋은 무료 강의가 많아서 다들 한번 방문해 보시는 걸 추천 드려요.

 

 

 

개발자 유미

백엔드 개발자 유미 - 실습 위주 진행 (개념적인 부분은 공식 Docs 참조 및 개인 학습 바랍니다!) - 간혹 댓글 알림이 안오는 경우가 있습니다.

www.youtube.com

 

HttpBasic 인증

로그인 방식

  • formLogin → 지금까지 우리가 한 것 (특정 html login 페이지를 만들어 data 를 서버에 post로 넘기는 방식)
  • httpBasic → 아이디와 비밀번호를 Base64 방식으로 인코딩 한 뒤에 HTTP 헤더에 부착 하여 서버측으로 요청을 보내는 방식 (주로 마이크로서비스 아키텍쳐에서 사용한다.)

⇒ 이 역식 spring 독스 내에 Basic Authentication 문서를 참고하자

⇒ 기존 SecurityConfig 파일 코드에서 🔽

해당 부분으로 변경해주면

⇒ 로그인 페이지 요청 시 🔽

위와 같은 화면으로 로그인 요청을 하게 된다.


해당 정리 내용은 유튜버 개발자 유미님 영상을 보고 정리한 내용 입니다.

좋은 무료 강의가 많아서 다들 한번 방문해 보시는 걸 추천 드려요.

 

 

개발자 유미

백엔드 개발자 유미 - 실습 위주 진행 (개념적인 부분은 공식 Docs 참조 및 개인 학습 바랍니다!) - 간혹 댓글 알림이 안오는 경우가 있습니다.

www.youtube.com

 

InMemory 유저 정보 저장

  • 소수의 유저를 저장하는 좋은 방법
  • 토이프로젝트나 시큐리티 로그인 환경에서 소수의 회원 정보만 필요하고 DB를 따로 투자하기 힘든 경우 InMemory 방식으로 유저를 저장하게 된다.(회원 가입이 없다)

⇒ InMemoryUserDetailsManager 클래스를 통해 유저를 등록하면 된다.

⇒ 해당 부분은 spring 독스의 inMemory 방식을 찾아보자.


csrf enable설정 방법

csrf란?

  • CSRF (Cross Site Request Forgery) 로 요청을 위조하여 사용자가 원하지 않아도 서버측으로 특정 요청을 강제로 보내는 방식
  • 개발 환경에서 Security Config 클래스를 통해 csrf 설정을 disable 설정을 했지만 실제 서비스 환경에서는 csrf 공격을 방지하기위해 csrf disable 설정을 제거하고 추가적인 설정을 진행해야 한다.

(기존 개발 환경에서 🔽)

// 로그인을 하게 되면 csrf 라는 토큰이 필요한데 지금 과정에서는 disable 상태로 두고 개발함
http.csrf((auth) -> auth.disable());

(배포할 시에 수정 코드 🔽)

// 로그인을 하게 되면 csrf 라는 토큰이 필요한데 지금 과정에서는 disable 상태로 두고 개발함
// http.csrf((auth) -> auth.disable());

이렇게 csrf 코드를 주석 처리 해주면 로그인 기능을 사용 할 수 없게 된다.
해당 코드를 주석 처리 하게 되면 enable 처리가 된어 csrf 설정이 진행된다.
enable 처리가 되면 스프링 시큐리티는 CsrfFilter를 통해
POST, PUT, DELETE 요청에 대해 토큰 검증을 진행한다.

(그래서 csrf 토큰을 관리하는 시스템을 구축하여야 한다. 🔽)

(mustache- post 요청에서 설정 기준)

<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Login Page</title>
</head>
<body>

login page
<hr>

<form action="/loginProc" method="post" name="loginForm">
    <input id="username" type="text" name="username" placeholder="id"/>
    <input id="password" type="password" name="password" placeholder="password"/>
    <input type="hidden" name="_csrf" value="{{_csrf.token}}"/> 
    <!-- 위의 코드를 집어 넣어 post 요청 코드에 같이 보낸다. -->
    <input type="submit" value="login"/>
</form>

</body>
</html>

⇒ 해당 코드를 바로 실행 하면 csrf 가 null이어서 오류가 발생하게 된다.

application.properties 파일에 추가해주자 🔽

spring.mustache.servlet.expose-request-attributes=true

⇒ 그리고 csrf 설정을 해주면 get 방식의 로그아웃 처리를 post 방식으로 변경해주어야 로그아웃을 할 수 있다.

⇒ 그리고 JWT 방식을 사용할 경우 csrf를 enable 할 정도로 위험 부담이 없다.


해당 정리 내용은 유튜버 개발자 유미님 영상을 보고 정리한 내용 입니다.

좋은 무료 강의가 많아서 다들 한번 방문해 보시는 걸 추천 드려요.

 

 

개발자 유미

백엔드 개발자 유미 - 실습 위주 진행 (개념적인 부분은 공식 Docs 참조 및 개인 학습 바랍니다!) - 간혹 댓글 알림이 안오는 경우가 있습니다.

www.youtube.com

 

세션 설정 (소멸, 중복 로그인, 고정 보호)

  • 사용자가 로그인을 하게 되면 사용자의 정보는 SecurityContextHolder에 의해 서버 세션에 관리된다. → 이번 시간에는 세션의 소멸 시간, 아이디당 세션 생성 개수 설정하는것을 배울 예정이다.

세션 소멸 시간 설정

  • 세션 타임 아웃 설정 → 특정 요청을 수행한 뒤 설정 시간 만큼만 유지

(application.properties 파일에서 🔽)

server.servlet.session.timeout=3000

⇒ 위와 같이 작성하면 3000 초간 로그인이 유지된다.

다중 로그인 설정

  • 동일한 아이디로 다중 로그인을 진행할 경우 설정 방법
  • 공식 문서의 sesstion Management 를 통해 구현 방법이 나와있다.

(SecurityConfig.java 파일에서 🔽)

// 다중 로그인 설정
http.sessionManagement((auth) -> auth
        .maximumSessions(1) // <- 하나의 아이디에 동시접속 수
        .maxSessionsPreventsLogin(true) // <- true: 해당 값을 초과할 경우 새 로그인 차단
        // false: 기존 로그인 아웃 새로그인 진행
);

세션 고정 보호 (해커의 admin 세션 탈취를 보호)

  • sesstionManagement((auth) → auth.sessionFixation().none()); : 로그인 시 세션 정보 변경 안함
  • sesstionManagement((auth) → auth.sessionFixation().newSession()); : 로그인 시 세션 새로 생성
  • sesstionManagement((auth) → auth.sessionFixation().changeSessionId()); : 로그인 시 동일한 세션에 대한 id 변경 ⇒ 주로 이걸 사용하여 보호한다.
// 세션 고정 보호
http.sessionManagement((auth) -> auth.sessionFixation().changeSessionId());

해당 정리 내용은 유튜버 개발자 유미님 영상을 보고 정리한 내용 입니다.

좋은 무료 강의가 많아서 다들 한번 방문해보시는걸 추천 드려요.

 

 

개발자 유미

백엔드 개발자 유미 - 실습 위주 진행 (개념적인 부분은 공식 Docs 참조 및 개인 학습 바랍니다!) - 간혹 댓글 알림이 안오는 경우가 있습니다.

www.youtube.com

 

세션 사용자 아이디 정보 (세션 정보)

  • 회원 id 정보 가져오기
String id = SecurityContextHolder.getContext().getAuthentication().getName();

model.addAttribute("id", id); // <- model에 key 와 value 로 저장하여 프론트에 전달

⇒ 회원 id 의 경우 다음과 같이 간단하게 호출이 가능하다.

  • 회원 등급 정보 가져오기
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

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

model.addAttribute("role", role); // <- model에 key 와 value 로 저장하여 프론트에 전달

⇒ 그런데 role 에 경우 위 처럼 긴 과정을 거쳐 갖고 오게 된다. 이유 좀 알고 싶다.


해당 정리 내용은 유튜버 개발자 유미님 영상을 보고 정리한 내용 입니다.

좋은 무료 강의가 많아서 다들 한번 방문해 보시는 걸 추천 드려요.

 

개발자 유미

백엔드 개발자 유미 - 실습 위주 진행 (개념적인 부분은 공식 Docs 참조 및 개인 학습 바랍니다!) - 간혹 댓글 알림이 안오는 경우가 있습니다.

www.youtube.com

 

+ Recent posts