#SSL인증서



포켓몬스터 게임을 하다보면

통신교환을 해야 최종진화가 되는 포켓몬들이 있다.

나는 어떻게 하는지도 모르고

방법도 귀찮아서 통신교환을 전혀 하지 않아

후딘, 강철톤, 괴력몬 등을 한번도 키워본 적이 없다.

진화의 돌 진화나 레벨업 진화만 있었으면 좋겠다.



통신교환을 하는 것은 데이터를 주고 받는 행위이다.

근데 이 데이터를 주고 받을 때

누가 가로채거나 엿본다면 데이터가 유출된다.


HTTP 통신에서도 마찬가지이다.

HTTP 통신은 암호화되지 않은 방법으로 통신하기 때문에

데이터가 쉽게 유출된다.


HTTP의 보안상 문제점을 보완하기 위해 나온 것이 HTTPS이다.

HTTPS는 SSL 프로토콜 위에서 돌아가는 프로토콜을 말하는데

쉽게 말하자면

SSL 인증서를 이용하여 웹에서 데이터를 암호화하여 주고 받을 수 있게 해준다.

SSL 인증서의 역할은 2가지이다.

첫째, 인증서 정보를 통해서 신뢰할 수 있는 서버인지 인증하는 것

둘째, 공개키로 데이터를 암호화하는 것


우선 대칭키와 공개키에 대해서 알아둘 필요가 있다.

대칭키는 암호화, 복호화 모두 하나의 키로 가능하다.

그렇기 때문에 대칭키가 유출이 되면 암호화 하는 의미가 없어진다.


반대로 공개키는 2개의 키 쌍으로 이루어져있다.

보통 다른 사람에게 공개하는 것을 공개키

나만 알고있는 것을 비밀키라 한다.

하나의 키로 암호화를 하게 되면

복호화는 다른 하나의 키로만 가능하다.

두 개의 키 모두 암호화, 복호화가 되지만

자신의 키로 암호화한 것을 복호화할 수 없고

다른 키로 암호화가 된 것만을 복호화 할 수 있다.


공개키와 대칭키에 대해 알아봤다면

SSL 인증서의 동작과정을 살펴보자.



첫번째 과정은 핸드쉐이킹이다.

이 과정은 본격적으로 데이터를 주고 받기 전에

서로가 지원하는 암호화 방식을 알아내고

데이터를 암호화하는 세션 키를 찾는 과정이다.


클라이언트가 서버에 접속을 하게 될 때

클라이언트는 자신이 사용할 수 있는 암호화 방식과

클라이언트가 생성한 랜덤 데이터를 서버에 전송한다.


서버는 클라이언트에게 받은 암호화 방식 중 자신이 지원가능한 암호화 방식을 고른다.

그리고 다시 클라이언트에게

서버가 고른 암호화 방식과

서버가 생성한 랜덤 데이터,

비밀키로 암호화된 SSL 인증서를 전송한다.


클라이언트는 SSL 인증서가

CA에 의해 발급된 인증서인지 확인하고

이미 내장된 CA의 공개키를 이용해 SSL 인증서를 복호화한다.

복호화가 정상적으로 되었다면

해당 서버는 CA에서 인증된 SSL 인증서를 가진 서버라는 것을 인증되는 것이다.


해당 서버가 신뢰할 수 있는 판단이 되는 순간

클라이언트는 자신이 생성한 랜덤 데이터와 서버가 생성한 랜덤 데이터를 조합하여

pre master secret이라는 키를 생성한다.

pre master secret을 공개키로 암호화를 하여 서버에 보내면

서버는 pre master secret을 비밀키로 복호화를 하게 된다.


클라이언트와 서버 모두 pre master secret을 이용하여 master secret을 만들고

master secret을 이용하여 세션 키를 만들게 된다.


여기까지가 핸드쉐이킹 단계이며

다음은 핸드쉐이킹 단계에서 만들었던 세션 키를 가지고

데이터를 암호화하여 주고 받는 세션 단계이다.


세션 단계에서 사용되는 세션 키는 대칭키로 암호화와 복호화가 모두 가능하다.

이미 핸드쉐이킹 단계에서 공개키를 이용한 방식으로 세션키를 만들었기 때문에

세션키를 외부로 노출할 일은 없게 된다.


결국 HTTPS는 공개키와 대칭키를 혼합하여 사용한다.

공개키는 대칭키를 암호화하여 전달하는데 사용이 되고

대칭키는 실제 데이터를 암호화하여 송수신하는데 사용이 된다.

공개키는 많은 컴퓨팅 파워를 사용하기 때문에

실제 데이터를 암호화하여 주고받는 방식은 대칭키를 이용한 방식이 더 좋다.


데이터 전송이 모두 끝나게 되면

통신에 사용하게 된 세션키는 폐기된다.


시간이 된다면 AWS를 이용하여

SSL 인증서로 HTTPS 서버를 구축해보는 것도 좋은 연습이 될 것 같다.


<참고자료>

https://opentutorials.org/course/228/4894


To be continued.........




Made by 꿩

'IT > 보안' 카테고리의 다른 글

[방화벽] 인바운드 & 아웃바운드  (0) 2022.01.31
[Spring] Google reCAPTCHA v3  (6) 2019.07.07
OAuth  (0) 2019.06.17
CSRF  (0) 2018.12.25
XSS 공격과 방어  (2) 2018.10.25

#Hash Table



해시란 무엇일까?

위키피디아에 따르면 해시 함수에 의해 얻어진 값을 해시라 한다.

그럼 해시 함수란?

임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하는 함수를 말한다.

마지막으로 해시 테이블이란?

해시를 이용한 테이블이겠지..ㅎ


해시 테이블은 해시 값이 인덱스가 되도록 원래의 키 값을 저장한 배열을 말한다.

좀 더 쉽게 풀어서 설명하자면

데이터를 어떠한 알고리즘으로 고유한 숫자를 만든다.

그리고 이것을 인덱스로 사용해 저장하는 방법이다.

배열을 사용하여 데이터를 저장하며

데이터 고유의 인덱스로 접근하게 되므로

매우 빠른 검색 속도를 갖는 자료구조이다.


이미 배열이 생성되어 있고 비어있는 곳을 굳이 채워 넣을 필요가 없기 때문에

삽입 또는 삭제도 효율적으로 수행할 수 있다.

물론 이때문에 충분한 메모리 공간이 필요하다는 단점이 있다.


포켓몬 이름으로 포켓몬의 정보를 해시방법으로 저장한다고 가정해보자.

샤미드, 쥬피썬더, 부스터를 각 이름을 해시 함수에 넣어 인덱스를 구하고

해당 인덱스에 데이터를 저장한다.

나중에 해당 포켓몬의 정보를 얻을 때

이름만 입력하면 바로 데이터를 가져올 수 있다.


그런데 해시 함수의 결과 값은 항상 고유값은 아니라는 문제가 있다.

중복값이 발생할 때를 해시 충돌이라 하고 보통 두가지 해결 방법이 제시된다.

다음과 같이 충돌이 난다고 가정해보자.

버터플과 도나리 모두 해시함수에 넣었을 때

동일한 인덱스 값이 나온다.


이처럼 해시 충돌에 대한 첫번째 해결 방법은 Separate Chaining 방식이 있다.

이는 나중에 들어온 데이터를 추가하는 방법이다.

LinkedList 또는 Tree를 사용하는 방식이 있다.

이 둘을 사용하는 기준은 버킷에 할당된 key-value 쌍의 개수이다.

트리는 기본적으로 메모리 사용량이 많고

데이터 개수가 적을 때

Worst Case를 살펴보면 이 둘의 성능 상 차이가 거의 없으므로

메모리 측면에서 볼 때 LinkedList를 사용하는 것이 맞다.


LinkedList를 이용하면 다음과 같이 저장이 된다.


두번째 방법은 Open Address 방식이 있다.

해시 충돌이 발생하면 다른 해시 버킷에 해당 데이터를 삽입하는 방식이다.

이 방법에서 또 3가지로 나뉘어진다.


1. Linear Probing: 순차적으로 비어있는 버킷 탐색

2. Quadratic Probing: 선형 탐사와 비슷하지만 폭을 제곱수로 넓게 탐색

3. Double hashing Probing: 해시 출동 발생 시 2차 해시 함수 이용하여 새로운 주소 할당


 이중 Linear Probing으로 충돌을 해결하면 다음과 같이 저장된다.


그렇다면 어떤 기준을 가지고 Separate Chaining과 Open Addressing을 사용해야 할까?

둘 모드 Worst Case O(M)이다.

그러나 Open Addressing은 연속된 공간에 데이터를 저장하기 때문에

Separate Chaining보다 캐시 효율이 높다.


따라서 데이터 개수가 적다면 Open Addressing이 성능이 좋지만

데이터 개수가 커질수록 Worst Case 발생빈도가 높아지기 때문에

Open Addressing이 Separate Chaining보다 느려진다.

Separate Chaining은 충돌 빈도를 보조 해시 함수를 이용하여 조절할 수 있기 때문에

Worst Case에 가까운 일이 발생하는 것을 줄일 수 있다.

(보조 해시 함수는 key의 해시 값을 변형하여 해시 충돌 가능성을 줄이는 것이다)


해싱에는 정적 해싱과 동적 해싱이 있다.

정적 해싱은 데이터를 입력하기 전에 일정 크기의 메모리를 미리 할당받는 것이며

해시 테이블의 크기가 할당된 메모리의 크기를 넘어갈 수 있다.


이러한 문제점을 해결하기 위해서 동적 해싱이 등장했는데

동적해싱은 오버플로우 발생시 버킷 안의 엔트리들 재조정하고

동적으로 해시 테이블의 크기를 변화시키면서 성능을 높여주는 방법이다.


동적 해싱은 데이터가 증가해도 검색의 성능이 유지되고 메모리 낭비를 줄일 수 있다는 장점이 있지만

로직이 복잡하고 버킷을 쪼개거나 합치는 과정 중에 부하가 발생하는 단점도 가지고 있다.


참고

https://k39335.tistory.com/18

https://d2.naver.com/helloworld/831311

http://blog.naver.com/PostView.nhn?blogId=skyjjw79&logNo=220875535595&parentCategoryNo=&categoryNo=61&viewDate=&isShowPopularPosts=true&from=search

To be continued.........




Made by 꿩













#[Firebase] FCM 푸시 메세지 - Notification



프로젝트 도중 Firebase FCM을 적용하다가

내가 올린 포스팅으로는 설명이 좀 부족하다는 죄책감이... ㅎ

이번 글에서는

기존 포스팅에 올린 것을 좀 더 상세히 설명할 것이다.


부디 잘 참고해서

적용하기 바람 ㅎㅎ


1. firebase-messaging-sw.js

firebase-messaging-sw.js는 service worker 파일이다.

크롬에서 F12 -> Application -> Service Workers에

firebase-messaging-sw.js이 있다면 정상작동하는 것이다.



FCM을 통해 푸시메세지를 보냈을 때

사용자가 브라우저를 꺼놓거나 다른 페이지 화면을 보고 있을 경우

이 service worker에 등록되어있는 파일이 작동하는 것이다.


2. Background

다음은 저번 포스팅때 올린 코드의 일부분이다.


1
2
3
4
5
6
7
8
9
10
11
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function(payload){
 
    const title = "Hello World";
    const options = {
            body: payload.data.status
    };
 
    return self.registration.showNotification(title,options);
});
 
cs


FCM으로 데이터가 들어오면 Notification을 보여주는 코드이다.

BackgroundMessageHandler라는 용어를 볼 때,

백그라운드 상태일 때 동작한다는 것을 알 수 있다.


3. foreground

그렇다면 백그라운드의 반대인 포그라운드 상태일 때는???

그 동작을 onMessage에서 관리한다.

다음은 포그라운드 상태일 때 메세지 동작 코드이다.


1
2
3
4
5
6
7
8
9
messaging.onMessage(function(payload){
        console.log('onMessage: ', payload);
        var title = "고라니 서비스";
        var options = {
                body: payload.notification.body
        };
        
        var notification = new Notification(title, options);
});
cs


이 코드를 원하는 html 페이지에 삽입한다면

해당 페이지로 직접 <div>를 설정하여 실시간으로 메세지를 보여주던가

alert창 또는 나처럼 Notification을 새로 생성해도 된다.

브라우저 마다 Notification 지원기능이 다르기 때문에 다음 표를 참고하기 바란다.



4. options

Notification에는 다양한 옵션을 지정해 줄 수 있다.

https://developer.mozilla.org/en-US/docs/Web/API/notification/Notification

이 링크에 들어가면 굉장히 많은 옵션을 Notification에 줄 수 있다는 것을 알 수 있다.

난 그중에서 icon 옵션을 써봤는데

코드는 option에 icon만 추가해주면 된다.


1
2
3
4
5
6
7
8
9
10
messaging.setBackgroundMessageHandler(function(payload){
 
    const title = "고라니 서비스";
    const options = {
            body: payload.notification.body,
            icon: payload.notification.icon
    };     
    
    return self.registration.showNotification(title,options);
});
cs


icon 주소를 잘 보냈다면

다음처럼 푸시메세지에 이미지가 들어갈 것이다.



동그랗게 고라니 그림이 들어간 것을 볼 수 있다.

참고로

나는 웹 앱을 만들어서 AWS를 이용해 서버를 올렸다.

그래서 스마트폰으로 푸시메세지를 테스트 할 수 있었다.

모바일 환경과 데스크톱 환경은 생각보다 다른 점이 많아서

정말 며칠동안 FCM 하나가지고 삽질을 했다 ㅠㅠ

단순히 기능이 된다고 끝이 아니라

기능 구현하는 코드를 이해해야

해당 기능을 자신의 프로젝트에 맞게 구현할 수 있다는 것을 명심해야 한다.



To be continued.........




Made by 꿩

'Database > Firebase' 카테고리의 다른 글

[Firebase] FCM 푸시 메세지  (1) 2018.12.09

#CSRF



CSRF는 Cross-site request forgery로 사이트 간 요청 위조라 불린다.


위키피디아의 설명에 의하면,

사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위를

특정 웹사이트에 요청하게 되는 공격이라 한다.



나만의 언어로 바꿔보자면

인터넷뱅킹에 로그인한 상태에서

메일을 들어가보니까

평소 내가 사고 싶었던 100cm 냐옹 인형을 싸게 구입할 수 있다는 광고메일이 왔어.

나는 궁금해서 링크를 클릭하겠지?

나는 냐옹 인형을 구입할 수 있는 웹사이트를 요청했지만

알고 봤더니

해커에게 돈을 송금한 요청이 보내진거지.


옥션 사례가 대표적인데

관리자가 로그인을 한 상태이고

자주 가는 사이트가 있었다.

공격자가 사이트에 악성스크립트를 넣은 글을 올렸고

관리자가 그 글을 본 순간

아이디와 비밀번호가 해커는 관리자권한을 획득했고

해커는 관리자로 로그인을 하여 개인정보 데이터를 빼내온 것이다.

XSS와 CSRF는 비슷하면서도 다르다.

둘 모두 악성 스크립트를 이용한다는 점은 동일하지만

XSS는 사용자의 화면에서 악성 스크립트가 실행되는 것이고

CSRF는 사용자 권한을 이용해 악성 스크립트를 서버에 요청을 한다는 점이 다르다.


마치

도둑이 내 물건을 훔쳐간 것과

보이스피싱처럼 내가 도둑에게 돈을 송금해 준 것

이 차이랄까?



물론 CSRF 공격을 막는 방법도 있다.

그 중 하나가 CSRF 토큰을 이용한 방법이다.

CSRF 토큰은 랜덤으로 들어가는 키값인 난수이다.


화면 호출 시 세션에 CSRF 토큰인 난수값이 저장된다.

해당 화면에 대한 요청이 들어올 때

CSRF 토큰 값과 비교해서

토근 값이 일치하면 요청을 실행하고

그렇지 않으면 해당 요청을 막는 방법이다.



앞서 언급한 예시를 이용해 설명해본다면

인터넷뱅킹에 로그인한 상태로 메일의 링크를 클릭했어

근데 그 링크는 해커에게 돈을 송금하도록 요청하는 링크야

서버에서 해커에게 송금하라는 요청을 받았는데

송금하는 화면에서 생성되는 CSRF 토큰을 검사해

근데 해커가 미리 지정한 메일의 링크에 CSRF 토큰인 난수값이 없으면

요청이 금지되는 거지


해커 입장에서는 CSRF 토큰은 난수값으로

랜덤으로 생성되므로 알 수 있는 방법이 없지

그래서 메일 링크에 CSRF 토큰값을 넣을 수가 없어

물론 천재 해커들은 뚫을 방법을 찾을 수도 있겠지만....ㅎ

내가 프로젝트를 수행하면서

CSRF 토큰을 지정하는 방법은 두가지이다.


첫번째는 input hidden으로 넣는방법

1
<input type="hidden" id="csrfToken" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
cs

두번째는 ajax요청시 header에 csrf 토큰을 넣는 방법(javascript)

1
2
3
4
5
6
7
8
9
10
11
<meta id="_csrf" name="_csrf" th:content="${_csrf.token}"/>
<meta id="_csrf_header" name="_csrf_header" th:content="${_csrf.headerName}"/>    
    
    //여기부터는 자바스크립트 
    var csrftoken = $('#_csrf').attr('content');
    var csrfheader = $('#_csrf_header').attr('content');
 
                    //ajax에 추가해야함.
                    beforeSend: function(xhr) {
                        xhr.setRequestHeader(csrfheader, csrftoken);
                    },
cs



To be continued.........




Made by 꿩




'IT > 보안' 카테고리의 다른 글

[방화벽] 인바운드 & 아웃바운드  (0) 2022.01.31
[Spring] Google reCAPTCHA v3  (6) 2019.07.07
OAuth  (0) 2019.06.17
SSL인증서  (0) 2019.03.21
XSS 공격과 방어  (2) 2018.10.25

#솔라리턴



얼마전 생일이 지나서

생일기념으로 친구가 점성술로 1년 운세를 봐주었다.


점성술에서는 1년 운세가 바뀌는 때가 바로 생일이다.

생일이 지나면서 프로펙션과 솔라리턴이 바뀌기 때문이다.



먼저 2018년을 되돌아보자면

참 되는 일이 없었다.


내가 무엇을 해야할지 몰라 우왕좌왕 했으며

정말 말도 안되는 것들에도 손을 담가도 보고 시도해보면서

소득도 없고 성과도 없었다.

1년이 텅 비어버린 것만 같다.


이럴 줄 알았으면

빡세게 알바라도 하면서 돈을 모아

여행이라도 갈껄이라는 후회가 남는다.


문제는 2018년보다 2019년이 더 안좋아보인다는 것이다.


뭐 친구는 조금.... 긍정적으로 얘기해주지만

친구 옆에서 점성학 이야기를 들은지 어언 3년째...

그동안 나도 궁금해서 몇몇 카페에도 가입하고 블로그 글도 자주 봤다.

서당개 3년이면 풍월을 읊는다고 했다.

상세히 보지는 못하지만

엄청 안좋거나 엄청 좋은 것 정도는 구분할 수 있다.


내년 프로펙션은 3하우스 사자자리로

태양이 부각되는 해이다.

사실 프로펙션 로드만 보면 내 태양이 나쁘지 않아 괜찮다고 볼 수도 있는데

솔라리턴에서 화성과 3도 차이로 스퀘어 각을 맺고 있고

내가 지금 피르다르 수성, 화성이라는 점도 걸린다.

더 큰 문제는 화성이 솔라리턴 앵글에 있다는 것....


나에겐 화성이란 마치 흉악범같은 존재로서

폴하고 항성 프로키온까지 더해져

마치 연쇄살인마를 보는 느낌이 든다.

어쨌든 단점이 가득했던 작년과는 다르게

올해에는 좋은 점이 있다는 한줄기 희망이 있지만

그 희망도 하자가 있다는 것이다.


안타깝고 슬픈 현실이다.


가끔씩

내 차트는 왜이리 안좋은 걸까?라는 생각을 한다.


뭐 어쩌겠어.

이렇게 태어났는데

이렇게 살아야지.

흑...



아무래도 주위에 운명학을 공부하는 친구가 있어서

나도 관심이 생겨

몇몇 술사들에게 내 생시로 운명을 점쳐보았다.


사주는 망가진 금수상관격이라

좋아지려다 말아먹은 사주란다.


사주 봐준 사람 말에 따르면

옛날 말로

굶지는 않고 살 팔자다

네?!

요즘은 굶는 사람들이 있나?

아니면

굶지 않는다는 것에 만족해야하나....?


예전에 점성술 봐준 사람 말에 따르면

만 29세즈음(토성리턴) 까지는 뭘 해도 안된다!!!

즈기요......

그럼 그때까지

손가락만 쪽쪽 빨고 있으라는 건가요...?


물론 내 인생도 잘 풀리는 때가 있다고 하지만

그건 참 머나먼 훗날..

과연 오기는 할까?


그냥 지금은 나의 시기가 아닌가보다하며

누가 돌을 던지면 조용히 맞고

누가 물을 뿌리면 샤워한다 생각하고

누가 욕을 하면 겸허하게 욕을 씹어주고

인생을 방관하며 살아야겠다.


올해와 마찬가지로

다음 2019년도

참 걱정되는 운세이긴 하지만

최대한 즐겁게 보내봐야겠다.

To be continued.........




Made by 꿩



'일상' 카테고리의 다른 글

[일상] 헬스장 창문 너머  (1) 2024.03.17
[일상] 청룡의 해가 들어오고 있어요!  (0) 2024.02.01
[일상] 나의 신(神)  (0) 2021.12.12
[일상] 명동성당 아침  (2) 2018.10.30
[일상] 북한산 3인방  (1) 2018.10.27

#[Firebase] FCM 푸시 메세지



이 글을 읽고 있는 당신!!!

FCM을 이용해 푸시 메세지를 보낸 적이 있는가?

완전 짱 신기함!!!!

쉽고

빠르게

대량 푸시를 하여

누군가에게 차단당할 수 있다!!!

ㅎㅎㅎ

장난이고

이번 주는 Firebase FCM 기능을 이용한 푸시 메세지를 포스팅하려 한다.


생각보다 간단하고 무료이므로

관심없는 이들도

재미삼아 만들어 보는 것도 좋을 듯하다.


작동원리를 간단히 설명하자면

당신의 컴퓨터에서 Firebase Cloud에 메세지를 보낸다.

당연히 메세지를 보낼 때

당신이 누구인지?, 누구에게 보낼 건지?, 메세지 내용 등

을 포함해서 보낸다.

그러면 Firebase가 해당 컴퓨터나 스마트폰에 푸시 메세지를 날려준다.


1. Firebase 계정만들기

Firebase FCM을 이용하므로 당연히 계정을 만들어야한다.

(무료 요금제 - Spark 요금제)


계정을 만들었으면 프로젝트를 추가한다.

위치는 대한민국이 있으니 설정하고 긍정적으로 체크하고 프로젝트를 만든다.


2. 푸시 메세지를 보내기 위한 정보 찾기

왼쪽 위의 설정버튼에서 프로젝트 설정을 클릭한다.



클라우드 메세징 탭을 보면

당신의 서버 키가 보일 것이다!!

이걸 복사하여 메모장에 잠시 가져다 놓자


다시 홈 화면으로 돌아오면

여기서 맨 오른쪽 거 </>라고 생긴 것을 클릭하자

이 버튼이 웹 앱에 Firebase를 추가하는 버튼이다.


그럼 다음과 같은 창이 뜰 것이다.

이것도 복사를 해 메모장에 일단 보관해놓자!


3. html 파일 만들기

html 파일을 만드는 이유는 토큰 값을 얻기 위해서다.

토큰 값은 쉽게 말해 바로 누구에게 보낼 지 알려주는 값이다.

나도 자세히는 모르지만

약간 주소 같은 느낌이 들었다.

주소를 알아야 택배 아저씨가 택배를 가져다 주니까...ㅎ


근데 스마트폰의 토큰 값은 바뀌지 않고

데스크 톱 토큰 값은 한번 껏다키니까 바뀌어 있었다.

이 부분에 대해서는 더 공부해야 할 듯 싶다(과연? ㅎ)


오늘 포스팅은 intellij로~

Web Application으로 프로젝트를 만든다.

아 !! 톰캣 꼭 설치되어 있어야 한다.


프로젝트를 만들고나면

아무 html 파일과 firebase-messaging-sw.js를 만들자!

(주의!) firebase-messaging-sw.js 이거 글자 틀리면 안된다!!


자바스크립트 파일(firebase-messaging-sw.js)은 다음과 같이 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/4.8.1/firebase-messaging.js');
 
// Initialize Firebase
var config = {
  apiKey: "enter api key",
    authDomain: "test-4e9a6.firebaseapp.com",
    databaseURL: "https://test-4e9a6.firebaseio.com",
    projectId: "test-4e9a6",
    storageBucket: "test-4e9a6.appspot.com",
    messagingSenderId: "90275796882"
};
firebase.initializeApp(config);
 
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function(payload){
 
    const title = "Hello World";
    const options = {
            body: payload.data.status
    };
 
    return self.registration.showNotification(title,options);
});
cs


html 파일도 다음과 같이 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
</head>
<body>
 
<h1 id="order"></h1>
 
<h1> 파이어 메세지</h1>
<script src="https://www.gstatic.com/firebasejs/5.5.9/firebase.js"></script>
<script>
    // Initialize Firebase
    var config = {
        apiKey: "API KEY를 넣어주세요~~~~~~~~~~~~~~~~~~~~~~~~",
        authDomain: "test-4e9a6.firebaseapp.com",
        databaseURL: "https://test-4e9a6.firebaseio.com",
        projectId: "test-4e9a6",
        storageBucket: "test-4e9a6.appspot.com",
        messagingSenderId: "90275796882"
    };
    firebase.initializeApp(config);
 
    const messaging = firebase.messaging();
 
    //token값 알아내기
    messaging.requestPermission()
        .then(function(){
            console.log("Have permission");
            return messaging.getToken();
        })
        .then(function(token){
            console.log(token);
        })
        .catch(function(arr){
            console.log("Error Occured");
        });
 
 
</script>
 
</body>
</html>
cs

apikey에 자신의 api key를 넣고

톰캣을 실행한다.

html 파일에 들어가면


다음과 같이 토큰값이 정상적으로 나오는 것을 확인할 수 있다.

이 토큰 값을 메모장에 옮겨놓는다.


4. 푸시 메세지 보내기

빨리 자고싶어서.....

메세지 보내는 html 파일 만들지는 않고

간단히 Restlet으로 해보려고 한다ㅎㅎ


Authorization에 서버 키를 넣고

"to" : 에는 토큰 값을 넣는다.


send 버튼을 누르면

겁나 빨리

푸시 메세지가 뜨는 것을 볼 수 있다.


친구에게 푸시 메세지를 보내고 싶다면

자신의 서버를 열어놔야 한다.


서버를 열어 해킹당할까봐 무섭다면

Firebase의 Hosting 서비스를 사용할 수 있다.


아까 만든 html과 js 파일을 hosting해놓고

친구가 호스팅 주소에 들어가 토큰 값을 알려준다면

당신은 그 친구에게 무한 푸시를 할 수 있다.


물론 차단하면 모든 노력은 물거품이 되지만... ㅎㅎ


안드로이드까지는 무리없이 웹으로 쉽게 푸시가 가능하다.

그러나 ios는 인증 과정을 거쳐야 해서 까다롭다.

역시 나는 안드로이드가 좋다.


언젠간...

Firebase FCM 푸시 메세지를 호스팅하는 글을 올려야 할 듯 싶다ㅎㅎ




To be continued.........




Made by 꿩












'Database > Firebase' 카테고리의 다른 글

[Firebase] FCM 푸시 메세지 - Notification  (2) 2019.01.06

#[Spring Boot] 카카오페이 API



생각보다 카카오페이 API에 대한 글이 적어서 포스팅을 해보려한다.


스프링 부트에서 카카오페이를 붙이는 데 도움이 되었으면 한다.

본 글은 단건결제 프로세스만 설명할 것이다.


먼저 Kakao Developers를 참고해야한다.

https://developers.kakao.com/docs/restapi/kakaopay-api

카카오페이 API가 어떤 데이터를 요청받고 주는지에 대해 쓰여있다.

----------------------------------------------------------------------------------------------------------------

먼저 결제준비 단계이다.

request는 카카오페이에서 요구하는 정보이다.

해석해보자면

POST방식으로 https://kapi.kakao.com + /v1/payment/approve란 호스트(url 주소)로

Authorization(권한)과 Content-Type을 보내라는 것이다.

이 부분이 header에 해당되는 내용이고

밑에 있는 키와 설명, 타입으로 되어있는 부분은 body로 보내면 된다.


권한은 어디서 얻을 수 있을까?

먼저, KakaoDevelopers에 로그인을 해야한다.

내 애플리케이션 -> 개요 -> 앱정보 -> 앱 키 표시를 눌렀을 때 나오는

admin 키가 바로 권한 키이다.


주의할 것은 바로 밑 플랫폼에서

다음과 같이 설정해야한다.


----------------------------------------------------------------------------------------------------------------

kakaoPay.html파일을 만들고 post방식으로 보내는 버튼을 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
 
<h1> kakaoPay api 이용하기 </h1>
 
<form method="post" action="/kakaoPay">
    <button>카카오페이로 결제하기</button>
</form>
 
 
</body>
</html>
cs


----------------------------------------------------------------------------------------------------------------

이제  KakaoPay 클래스를 만들어본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package org.salem.service;
 
import java.net.URI;
import java.net.URISyntaxException;
 
import org.salem.domain.KakaoPayApprovalVO;
import org.salem.domain.KakaoPayReadyVO;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
 
import lombok.extern.java.Log;
 
@Service
@Log
public class KakaoPay {
 
    private static final String HOST = "https://kapi.kakao.com";
    
    private KakaoPayReadyVO kakaoPayReadyVO;
    
    public String kakaoPayReady() {
 
        RestTemplate restTemplate = new RestTemplate();
 
        // 서버로 요청할 Header
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization""KakaoAK " + "admin key를 넣어주세요~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!");
        headers.add("Accept", MediaType.APPLICATION_JSON_UTF8_VALUE);
        headers.add("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
        
        // 서버로 요청할 Body
        MultiValueMap<StringString> params = new LinkedMultiValueMap<StringString>();
        params.add("cid""TC0ONETIME");
        params.add("partner_order_id""1001");
        params.add("partner_user_id""gorany");
        params.add("item_name""갤럭시S9");
        params.add("quantity""1");
        params.add("total_amount""2100");
        params.add("tax_free_amount""100");
        params.add("approval_url""http://localhost:8080/kakaoPaySuccess");
        params.add("cancel_url""http://localhost:8080/kakaoPayCancel");
        params.add("fail_url""http://localhost:8080/kakaoPaySuccessFail");
 
         HttpEntity<MultiValueMap<StringString>> body = new HttpEntity<MultiValueMap<StringString>>(params, headers);
 
        try {
            kakaoPayReadyVO = restTemplate.postForObject(new URI(HOST + "/v1/payment/ready"), body, KakaoPayReadyVO.class);
            
            log.info("" + kakaoPayReadyVO);
            
            return kakaoPayReadyVO.getNext_redirect_pc_url();
 
        } catch (RestClientException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (URISyntaxException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        return "/pay";
        
    }
    
}
 
cs


1) cid는 가맹점 코드로 카카오페이에 연락해서 받아야 한다.

Test 코드이므로 TC0ONETIME를 넣었고

실결제를 하려면 카카오페이와 제휴 후 받은 cid 코드를 넣으면 된다.


2) Authorization에 위에서 설명한 admin 키를 넣어야 한다.


3)  body 부분에는 내가 결제로 지정할 데이터들을 넣는다.

kakao Developers에 필수라고 써있는 파라미터들은 꼭 넣어줘야한다.


4) HttpEntity<MultiValueMap<StringString>> body = new HttpEntity<MultiValueMap<StringString>>(params, headers);

hearder와 body를 붙이는 방법이다.


5) kakaoPayReadyVO = restTemplate.postForObject(new URI(HOST + "/v1/payment/ready"), body, KakaoPayReadyVO.class);

RestTemplate을 이용해 카카오페이에 데이터를 보내는 방법이다.

post방식으로 HOST + "/v1/payment/ready"에 body(header+body)정보를 보낸다.

정보를 보내고 요청이 성공적으로 이루어지면 카카오페이에서 응답정보를 보내준다.

KakaoPayReadyVO.class는 응답을 받는 객체를 설정한 것이다.

response로 위와 같은 데이터가 들어오므로 이를 객체로 받기 위한 자바 빈을 만들었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.salem.domain;
 
import java.util.Date;
 
import lombok.Data;
 
@Data
public class KakaoPayReadyVO {
    
    //response
    private String tid, next_redirect_pc_url;
    private Date created_at;
    
}
 
cs

안드로이드나 ios는 사용하지 않으므로 빼도 상관없다.


6) return kakaoPayReadyVO.getNext_redirect_pc_url();

마지막 return 값으로 redirect url을 불러와 결제가 완료되면 해당 주소로 가게끔 설정해 놓는다.

----------------------------------------------------------------------------------------------------------------

지금까지 만든 클래스가 잘 작동하도록 controller를 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package org.salem.controller;
 
import org.salem.service.KakaoPay;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
 
import lombok.Setter;
import lombok.extern.java.Log;
 
@Log
@Controller
public class SampleController {
    
    @Setter(onMethod_ = @Autowired)
    private KakaoPay kakaopay;
    
    
    @GetMapping("/kakaoPay")
    public void kakaoPayGet() {
        
    }
    
    @PostMapping("/kakaoPay")
    public String kakaoPay() {
        log.info("kakaoPay post............................................");
        
        return "redirect:" + kakaopay.kakaoPayReady();
 
    }
    
    @GetMapping("/kakaoPaySuccess")
    public void kakaoPaySuccess(@RequestParam("pg_token"String pg_token, Model model) {
        log.info("kakaoPaySuccess get............................................");
        log.info("kakaoPaySuccess pg_token : " + pg_token);
        
    }
    
}
 
cs

kakaoPaySuccess.html까지 만들어 놓으면

우선 카카오페이 결제가 정상적으로 작동이 될 것이다.

<url에 pg_token이 들어왔는지 확인하자!

ex) http://localhost:8080/kakaoPaySuccess?pg_token=8cf5a737f5fd9151f2ca>




지금까지는 카카오페이에 "갤럭시S9 제품 2100원 결제해주세요~"라고 요청했을때

카카오페이에서 알아서 결제하도록 만든 것이다.


결제가 완료되면 결제완료 창에

결제가 어떤 방식으로 이루어졌는지?

어떤 카드를 써서 결제를 했는지?

등등 결제정보를 카카오페이에서 받아와야하지 않겠는가?

----------------------------------------------------------------------------------------------------------------

이제 결제승인 단계로 가본다.

매커니즘은 결제준비 단계와 크게 다르지 않지만

결제완료 후 받아오는 pg_token과 tid가 필수적으로 있어야 한다.


동일하게 KakaoPay 클래스에 kakaoPayInfo메소드를 추가했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package org.salem.service;
 
import java.net.URI;
import java.net.URISyntaxException;
 
import org.salem.domain.KakaoPayApprovalVO;
import org.salem.domain.KakaoPayReadyVO;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
 
import lombok.extern.java.Log;
 
@Service
@Log
public class KakaoPay {
 
    private static final String HOST = "https://kapi.kakao.com";
    
    private KakaoPayReadyVO kakaoPayReadyVO;
    private KakaoPayApprovalVO kakaoPayApprovalVO;
    
    public String kakaoPayReady() {
 
        RestTemplate restTemplate = new RestTemplate();
 
        // 서버로 요청할 Header
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization""KakaoAK " + "admin key를 넣어주세요~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!");
        headers.add("Accept", MediaType.APPLICATION_JSON_UTF8_VALUE);
        headers.add("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
        
        // 서버로 요청할 Body
        MultiValueMap<StringString> params = new LinkedMultiValueMap<StringString>();
        params.add("cid""TC0ONETIME");
        params.add("partner_order_id""1001");
        params.add("partner_user_id""gorany");
        params.add("item_name""갤럭시S9");
        params.add("quantity""1");
        params.add("total_amount""2100");
        params.add("tax_free_amount""100");
        params.add("approval_url""http://localhost:8080/kakaoPaySuccess");
        params.add("cancel_url""http://localhost:8080/kakaoPayCancel");
        params.add("fail_url""http://localhost:8080/kakaoPaySuccessFail");
 
         HttpEntity<MultiValueMap<StringString>> body = new HttpEntity<MultiValueMap<StringString>>(params, headers);
 
        try {
            kakaoPayReadyVO = restTemplate.postForObject(new URI(HOST + "/v1/payment/ready"), body, KakaoPayReadyVO.class);
            
            log.info("" + kakaoPayReadyVO);
            
            return kakaoPayReadyVO.getNext_redirect_pc_url();
 
        } catch (RestClientException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (URISyntaxException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        return "/pay";
        
    }
    
    public KakaoPayApprovalVO kakaoPayInfo(String pg_token) {
 
        log.info("KakaoPayInfoVO............................................");
        log.info("-----------------------------");
        
        RestTemplate restTemplate = new RestTemplate();
 
        // 서버로 요청할 Header
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization""KakaoAK " + "admin key를 넣어주세요~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~!");
        headers.add("Accept", MediaType.APPLICATION_JSON_UTF8_VALUE);
        headers.add("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
 
        // 서버로 요청할 Body
        MultiValueMap<StringString> params = new LinkedMultiValueMap<StringString>();
        params.add("cid""TC0ONETIME");
        params.add("tid", kakaoPayReadyVO.getTid());
        params.add("partner_order_id""1001");
        params.add("partner_user_id""gorany");
        params.add("pg_token", pg_token);
        params.add("total_amount""2100");
        
        HttpEntity<MultiValueMap<StringString>> body = new HttpEntity<MultiValueMap<StringString>>(params, headers);
        
        try {
            kakaoPayApprovalVO = restTemplate.postForObject(new URI(HOST + "/v1/payment/approve"), body, KakaoPayApprovalVO.class);
            log.info("" + kakaoPayApprovalVO);
          
            return kakaoPayApprovalVO;
        
        } catch (RestClientException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (URISyntaxException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        return null;
    }
    
}
 
cs


1) kakaoPayApprovalVO = restTemplate.postForObject(new URI(HOST + "/v1/payment/approve"), body, KakaoPayApprovalVO.class);

응답정보를 받기 위해 KakaoPayApprovalVO 클래스를 만든다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.salem.domain;
 
import java.util.Date;
 
import lombok.Data;
 
@Data
public class KakaoPayApprovalVO {
    
    //response
    private String aid, tid, cid, sid;
    private String partner_order_id, partner_user_id, payment_method_type;
    private AmountVO amount;
    private CardVO card_info;
    private String item_name, item_code, payload;
    private Integer quantity, tax_free_amount, vat_amount;
    private Date created_at, approved_at;
    
    
}
 
cs

여기서 amount 와 card_info는 JSONObject로 전송받기 때문에

따로 AmountVO, CardVO라는 객체를 만들어 준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package org.salem.domain;
 
import lombok.Data;
 
@Data
public class AmountVO {
 
    private Integer total, tax_free, vat, point, discount;
}
 
package org.salem.domain;
 
import lombok.Data;
 
@Data
public class CardVO {
    
    private String purchase_corp, purchase_corp_code;
    private String issuer_corp, issuer_corp_code;
    private String bin, card_type, install_month, approved_id, card_mid;
    private String interest_free_install, card_item_code;
    
 
}
 
cs

----------------------------------------------------------------------------------------------------------------

Controller에서 model.addAttribute를 이용하여

화면 쪽에 정보를 전송한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package org.salem.controller;
 
import org.salem.service.KakaoPay;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
 
import lombok.Setter;
import lombok.extern.java.Log;
 
@Log
@Controller
public class SampleController {
    
    @Setter(onMethod_ = @Autowired)
    private KakaoPay kakaopay;
    
    
    @GetMapping("/kakaoPay")
    public void kakaoPayGet() {
        
    }
    
    @PostMapping("/kakaoPay")
    public String kakaoPay() {
        log.info("kakaoPay post............................................");
        
        return "redirect:" + kakaopay.kakaoPayReady();
 
    }
    
    @GetMapping("/kakaoPaySuccess")
    public void kakaoPaySuccess(@RequestParam("pg_token"String pg_token, Model model) {
        log.info("kakaoPaySuccess get............................................");
        log.info("kakaoPaySuccess pg_token : " + pg_token);
        
        model.addAttribute("info", kakaopay.kakaoPayInfo(pg_token));
        
    }
    
}
 
cs


----------------------------------------------------------------------------------------------------------------

이제 kakaoPaySuccess.html에 결제승인된 정보를 나타내보자.

Controller에서 보내온 데이터를 Thymeleaf를 이용해 표현했다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
 
카카오페이 결제가 정상적으로 완료되었습니다.
 
결제일시:     [[${info.approved_at}]]<br/>
주문번호:    [[${info.partner_order_id}]]<br/>
상품명:    [[${info.item_name}]]<br/>
상품수량:    [[${info.quantity}]]<br/>
결제금액:    [[${info.amount.total}]]<br/>
결제방법:    [[${info.payment_method_type}]]<br/>
 
 
 
<h2>[[${info}]]</h2>
 
</body>
</html>
cs


결제가 완료되면 화면은 다음과 같이 나올 것이다.




To be continued.........




Made by 꿩






# 추상클래스와 인터페이스



추상화란 형태를 잡는 것이다.

미술에서 추상화는 구체적인 형상을 나타내는 것이 아니라

점, 선, 면, 색과 같은 순수한 조형 요소로 표현한 것이다.

프로그래밍에서 추상화도 마찬가지이다.

구체적인 코드를 짜는 것이 아니라

기본 틀을 잡고 가는 것이다.


추상클래스와 인터페이스는 모두 추상화에 포함된다.

추상클래스는 메소드의 내용을 구현할 수 있는 것과 달리

인터페이스는 명시만 되어있다.

인터페이스가 추상클래스보다 더 추상화된 형태라고 보면 된다.


그렇다면 왜 추상화란 개념이 프로그래밍에 적용이 될까?

바로 다른 사람과의 커뮤니케이션 때문이다.


같이 프로그램 코드를 짜고 있는 상황에서

다른 사람이 어떤 이름의 객체, 메소드를 만드는 지 모른다면

다른 사람의 만든 객체를 내가 쓸 수 없다.

혹은 다른 사람의 일이 다 끝나야 내가 코드를 완성시킬 수 있다.


그러나 인터페이스를 이용하여 먼저 약속을 해놓으면

상대방의 코드 내용을 알지 못해도

나는 내 코드를 완성시킬 수 있다.

일반적으로는

인터페이스 -> 추상클래스 -> 구현클래스

이 3단계로 프로그램 코드를 완성했다.

그러나

자바 8버전 부터는 인터페이스에 디폴트 메소드가 생겼다.

인터페이스도 제한적이지만 메소드를 구현할 수 있게 되면서

추상클래스를 꼭 만들 필요성이 없어졌다.

그렇다면 추상클래스를 이제 쓸 필요가 없어졌는가?

아니다.

인터페이스가 추상클래스의 역할 전부를 대체할 수 없다.

인터페이스는 여전히 상수변수만을 쓸 수 있다.

그렇기에 객체변수를 선언할 필요가 있는 경우엔

추상클래스가 필요하다.


참고로 자바 8버전부터 디폴트 메소드로 인해

스프링에서 몇몇 추상클래스가 Deprecated 되었다.

한마디로 더이상 못쓴다는 것이다.


예를 들어,

WebMvcConfigurerAdapter라는 추상클래스가 있다.


자바 8버전을 베이스로 만든 스프링 5버전 부터는 쓸 수 없다고 나온다.

이 추상클래스가 Deprecated 된 이유는

앞서 말했듯이 바로 디폴트 메소드의 추가로 인해 생겼다.

혹여나 옛날 코드를 참고해 코드를 짜면 에러가 생길 수 있으므로 참고하길 바란다.


To be continued.........




Made by 꿩



'IT > Java' 카테고리의 다른 글

Spring Boot와 JSP  (0) 2019.08.02
다형성  (0) 2018.11.03
object vs instance vs class  (3) 2018.10.24

+ Recent posts