728x90

 

 

 

 

 

 이번 주 공부할 내용인 해시 테이블 자료구조에 대해 정리해보겠습니다. 먼저 해시 테이블과 해싱(Hashing)이 무엇인지 알아보겠습니다. 그다음 자바에서 어떻게 적용되어 있는지 알아본 후 대표적인 클래스와 그 메서드를 알아보겠습니다.

 

해시 테이블(Hash Table)

 해시 테이블이란 키와 값의 쌍으로 이루어진 자료구조입니다. 해당 자료구조에는 해싱이라는 과정을 통해서 데이터가 저장됩니다. 그리고 원리적으로는 데이터 양이 아무리 많아지더라도 자료의 검색과 삭제에 O(1)의 시간이 걸립니다. 때문에 커다란 데이터에서 특정한 값을 검색할 때 해시 테이블을 사용하는 것이 유리합니다.

 여기서 해싱(Hashing)이란 해시 함수(Hash Function)를 이용해 데이터를 저장하고 검색하는 기법을 말합니다. 해시 함수는 임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하는 함수를 말합니다. 예를 들어 해시 함수 중 하나인 SHA-256의 경우 해당 함수에 한 글자를 넣던 백 글자를 넣던 입력값의 길이에 상관없이 256비트의 결괏값이 출력됩니다. 좀 더 간단히는 아래의 그림과 같이 표현할 수 있습니다.

 해시 테이블은 자료 검색에 사용하기 위한 자료구조라 키값에 중복이 있으면 안 됩니다. 하지만 해시 함수는 어떤 값을 입력으로 넣든 간에 같은 길이의 결괏값을 반환하기 때문에 낮은 확률일지라도 서로 다른 값을 입력으로 넣었지만 같은 값이 출력되는 경우가 있습니다. 

 위의 그림과 같으며 이를 해시 충돌이라고 합니다. 해시 충돌을 방지하기 위해서 체이닝이나 이중 해시같은 방법이 사용됩니다. 다음과 같이 간단히 해시에 대해 알아보았습니다. 다음으로는 자바에서의 해시에 대해 알아보겠습니다.

 

자바에서의 해싱

 자바에서 해싱을 구현한 클래스로는 HashSet, HashMap, Hashtable 등이 있습니다. 이중 컬렉션 프레임워크가 등장하기 전에 사용하던 Hashtable은 이전 소스와의 호환성 문제로 남겨두고 있으니 이제는 HashMap을 사용하는 것이 좋습니다. 그리고 HashSet은 리스트와 달리 저장 순서를 유지하지 않습니다. 저장 순서를 유지하고자 한다면 LinkedHashSet을 사용하면 됩니다.

 자바에서 해싱을 사용하는 자료구조는 배열과 LinkedList의 조합으로 되어 있습니다. 해시 함수를 통해 나온 해시 코드는 키로써 배열의 인덱스가 되고 값은 해당 배열에 연결된 LinkedList에 저장됩니다. 때문에 키값으로 배열의 해당 인덱스에 있는 연결 리스트에 있는 값을 빠르게 찾을 수 있습니다.

 해시 함수에서 해시 충돌이라는 것에 대해서 이야기를 했었습니다. 자바에서는 해시 함수로 Object 클래스에 정의된 hashCode() 메서드를 사용합니다. 해당 메서드는 객체의 주소를 이용하는 알고리즘이라 모든 객체에 대해 유일한 값을 반환합니다. 때문에 해시 충돌의 걱정을 덜 수 있습니다. 그리고 String 클래스의 경우에는 오버라이딩한 hashCode() 메서드를 사용합니다. 이 메서드는 같은 내용의 문자열을 가졌다면 같은 해시 코드를 반환합니다. 이상으로 해싱은 마치고 클래스와 메서드에 대해 정리하겠습니다.

 

HashSet

  • 중복된 요소를 저장하지 않습니다.
  • 저장 순서를 유지하지 않습니다.

메서드 설명

boolean add(Obeject o) 새로운 객체를 저장합니다.
boolean addAll(Collection c) 주어진 컬렉션에 저장된 모든 객체를 추가합니다.
void clear() 저장된 모든 객체를 삭제합니다.
Object clone() 얕은 복사해 반환합니다.
boolean contains(Object o) 지정된 객체를 포함하고 있는지 알려줍니다.
boolean containsAll(Collection c) 컬렉션 c에 저장된 모든 객체를 포함하는지 알려줍니다.
boolean isEmpty() 비어있는지 알려줍니다.
Iterator iterator() iterator를 반환합니다.
boolean remove(Object o) 지정된 객체를 삭제합니다.
boolean removeAll(Collection c) 주어진 컬렉션에 저장된 모든 객체와 동일한 것을 삭제합니다.
boolean retainAll(Collection c) 주어진 컬렉션에 저장된 객체와 동일한 것만 남기고 삭제합니다.
int size() 저장된 객체의 수를 반환합니다.
Object[] toArray() 저장된 객체들을 객체 배열의 형태로 반환합니다.
Object[] toArray(Object[] a) 저장된 객체들을 주어진 객체 배열 a에 담습니다.

 

HashMap

메서드 설명

void clear() 저장된 모든 객체를 삭제합니다.
Object clone() 얕은 복사해 반환합니다.
boolean containsKey(Object key) 지정된 키가 있는지 알려줍니다.
boolean containsValue(Object value) 지정된 값이 있는지 알려줍니다.
boolean remove(Object key) 지정된 키로 된 저장된 값을 삭제합니다.
Object replace(Object key, Object value) 지정된 키의 값을 value 값으로 대체합니다.
boolean replace(Object key, Object oldValue, Object newValue) 지정된 키와 oldValue가 일치하는 경우에만 newValue 값으로 대체합니다.
int size() 저장된 객체의 수를 반환합니다.
Collection values() 저장된 모든 값을 컬렉션의 형태로 반환합니다.
Object put(Object key, Object value) 지정된 키와 값을 저장합니다.
void putAll(Map m) 맵에 저장된 모든 요소를 HashMap에 저장합니다.
Set keySet() HashMap에 저장된 모든 키를 Set으로 반환합니다.
boolean isEmpty() 비어있는지 알려줍니다.
Object get(Object key) 지정된 키의 값을 반환합니다. 못찾을 경우 null을 반환합니다.
Object getOrDefault(Object key, Object defaultValue) 지정된 키의 값을 반환합니다. 못찾을 경우 기본값으로 지정된 객체를 반환합니다.
728x90
728x90

이번 주 스터디에서는 자료구조 중 스택과 큐에 대해 공부하고 문제를 풀어보는 시간을 가졌습니다.

전에도 스택과 큐에 대해서 공부해봤기 때문에 구조적인 측면에서 이해는 문제가 없었습니다.

하지만 파이썬으로 구현했던 거라 자바로 구현하려고 하니 '어떤 클래스를 사용하고 어떤 메서드를 사용하지?'라는 생각이 들었습니다.

그래서 이걸 간단히 정리해 보려고 합니다.

스택과 큐의 자료구조와 메서드에 대해 정리하겠습니다.

 

스택(Stack)

스택은 LIFO(Last In First Out)의 특징을 가지고 있습니다.

이건 가장 마지막으로 들어간 데이터가 가장 첫 번째로 나오는 성질을 말합니다.

다르게 표현하면 가장 먼저 들어간 데이터가 가장 마지막으로 나오는 것을 뜻하기도 합니다.

 

재귀적인 함수, 알고리즘에 사용됩니다.

데이터를 삽입 및 삭제에 O(1), 탐색에 O(n) 시간이 걸립니다.

 

자바에서는 Stack 클래스를 구현하여 제공합니다.

Stack 클래스 메서드는 아래와 같습니다.

메서드 내용
 boolean empty() 스택이 비어있는지 알려줍니다.
 Object peek() 스택의 맨 위에 저장된 객체를 반환합니다.
객체를 꺼내지는 않습니다.
비어있을 때 사용할 경우 예외 오류를 발생시킵니다.
Object pop() 스택 맨 위의 저장된 객체를 꺼냅니다.
비었을 때 사용할 경우 오류를 발생시킵니다.
Object push(Object o) 스탬 맨 위에 o를 저장합니다.
int search(Object o) 스택에서 주어진 객체 o를 찾아서 그 위치를 반환합니다.
못찾으면 -1을 반환합니다.
배열과 달리 위치는 1부터 시작합니다.
Stack<Integer> st = new Stack<Integer>();

 

큐(Queue)

큐는 FIFO(First In First Out)의 특성을 가지고 있습니다.

이는 먼저 집어넣은 데이터가 먼저 나오는 성질을 말합니다.

때문에 데이터를 집어넣으면 뒤부터 순차적으로 생성되고 

뺄 때는 가장 처음 데이터부터 나오게 됩니다.

 

삽입 및 삭제에 O(1), 탐색에 O(n)의 시간이 걸립니다.

CPU 작업을 기다리는 프로세스, 스레드 행렬 또는 네트워크 접속을 기다리는 행렬

너비 우선 탐색(BFS), 캐시 등에 사용됩니다.

 

자바에서 큐의 경우에는 클래스가 아닌 인터페이스를 제공합니다.

물론 구현해 놓은 클래스도 있기 때문에 그중 하나를 사용하면 됩니다.

예시로 ArrayDeque, ConcurrentLinkedDeque, LinkedList가 있습니다.

Deque의 경우에는 앞뒤로 데이터를 넣고 빼는 메서드를 가지고 있습니다.

 

메서드는 아래와 같습니다.

메서드 내용
 boolean add(Object o) 지정된 객체를 큐에 삽입합니다.
성공하면 true를 반환합니다.
실패하면 오류를 반환합니다.
 Object remove() 큐에서 객체를 꺼내 반환합니다.
비어있을 경우 오류를 발생시킵니다.
 Object element() 삭제없이 요소를 읽어옵니다.
비어있을 경우 오류를 발생시킵니다.
 boolean offer(Object o) 큐에 객체를 저장합니다.
성공하면 true를 반환합니다.
 Object poll() 큐에서 객체를 꺼내서 반환합니다.
비어있으면 null을 반환합니다.
 Object peek() 삭제없이 요소를 읽어옵니다.
큐가 비어 있으면 null을 반환합니다.

위 세 개와 아래 세 개의 메서드는 같은 기능을 하지만 오류를 반환하고 하지 않는 차이가 있습니다.

Queue<Integer> q = new LinkedList<Integer>()

 

728x90
728x90

Java의 정렬 방법 중 Java.util.Arrays 클래스의 sort() 메서드를 사용해 보겠습니다.

 

sort() 메서드는 매개변수로 기본타입과 객체를 받을 수 있습니다.

그리고 시작 인덱스와 종료 인덱스를 추가로 받아 부분 정렬이 가능합니다.

추가로 기본적으로 오름차순 정렬되지만 내림차순으로도 정렬 가능합니다. 


문법

sort(primitive type array) 

  - 매개변수로 기본타입 배열을 이용합니다.

  - int[], double[], float[], long[], short[], byte[], char[] 

sort(primitive type array, int startIndext, int endIndex)

  - 기본타입에 시작 인덱스 값과 종료 인덱스 값을 지정합니다.

 

sort(Object[] o)

  - 매개변수로 객체를 받습니다.

  - Wrapper Class 값을 받을 수 있습니다.

sort(Object[] o, int startIndext, int endIndex)

 

sort(T[] a, Comparator<? super T> c)

  - 매개변수로 객체를 받으며 Comparator의 메서드에 따라 정렬할 수 있습니다.

  - 주로 Comparator.reverseOrder()를 사용해 내림차순으로 사용합니다.

sort(T[] a, int startIndext, int endIndex, Comparator<? super T> c)

 

예시

오름차순

int[] arr = {5, 3, 4, 1, 2};

Arrays.sort(arr);

String[] arr2 = {"peach", "apple", "banana"};

Arrays.sort(arr2);

Arrays.stream(arr).forEach(a -> System.out.println(a));
Arrays.stream(arr2).forEach(a -> System.out.println(a));

//결과
1
2
3
4
5
apple
banana
peach

내림차순

  - 기본타입의 배열을 내림차순으로 정렬하고 싶다면

기본타입 배열을 래퍼 클래스로 감싸야 내림차순으로 정렬할 수 있습니다.

Integer[] arr = {5, 3, 4, 1, 2};

Arrays.sort(arr, Comparator.reverseOrder());

String[] arr2 = {"peach", "apple", "banana"};

Arrays.sort(arr2, Comparator.reverseOrder());

Arrays.stream(arr).forEach(a -> System.out.println(a));
Arrays.stream(arr2).forEach(a -> System.out.println(a));

//결과
5
4
3
2
1
peach
banana
apple

 

 

728x90
728x90

일단 부트스트랩 5 모달창 다루는 공식 사이트입니다.

https://getbootstrap.com/docs/5.2/components/modal/#how-it-works

 

Modal

Use Bootstrap’s JavaScript modal plugin to add dialogs to your site for lightboxes, user notifications, or completely custom content.

getbootstrap.com

 

제가 원한 건 페이지에 접속 시 아래와 같이 바로 모달창이 뜨는 것이었습니다.

공식 예제에는 버튼을 주로 사용해서 혹시 필요하신 분이 있을까 올립니다.

 

<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" 
    aria-labelledby="exampleModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
            	<h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
            	<button type="button" class="btn-close" 
                data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
            ...
            </div>
            <div class="modal-footer">
            <button type="button" class="btn btn-secondary" 
            		data-bs-dismiss="modal">Close</button>
            <button type="button" class="btn btn-primary">Save changes</button>
            </div>
        </div>
    </div>
</div>

일단 모달창을 준비해주시고요. 해당 모달은 아래와 같이 생겼습니다.

아래는 페이지에 들어왔을 때 켜는 코드입니다. 쉽게 표현하기 위해서 jQuery를 사용했습니다.

<script type="text/javascript">
	$().ready(function(){
		const myModal = new bootstrap.Modal('#exampleModal', {});
		myModal.show();
	})
</script>
const myModal = new bootstrap.Modal('모달창ID', {옵션});

 

원할 때 숨기고 싶으시면 hide() 메서드를 사용하시면 됩니다.

<script type="text/javascript">
	const myModal = new bootstrap.Modal('#exampleModal', {});
    
    $().ready(function(){
		myModal.show();
	})
    
    function closeModal(){
    	myModal.hide();
    }
</script>

 

728x90
728x90

팀 프로젝트에서 사용시간이 지난 예약 내역에 대해 사용완료 처리를 해야 할 필요성이 있었습니다.

그 방법으로 spring @scheduled 어노테이션을 찾았습니다.

 

Maven 프로젝트이고 Java 17.0.2/ sts 3.9.18 버전을 사용중입니다.

 

@scheduled 어노테이션을 사용하기 위해서는 몇 가지 사전작업이 필요합니다.

spring < appServlet < servlet-context.xml에 하단의 빨간 줄로 표시한 것을 추가해주세요

xmlns:task="http://www.springframework.org/schema/task"
http://www.springframework.org/schema/task https://www.springframework.org/schema/task/spring-task.xsd"

 

그리고 Bean 등록이 필요한데 저는 service 패키지 하위에 Scheduler 클래스를 생성했습니다.

이걸 고려하면서 Scheduler Bean등록을 해주세요. Bean 등록도 servlet-context.xml에 해주시면 됩니다.

잘 모르겠다 하시는 분들은 최하단의 공식 가이드를 봐주세요.

 

클래스에 @Component, 사용하실 메서드에 @Scheduled 어노테이션을 잊지 마세요.

@Scheduled 뒤 옵션에는 여러 가지가 있지만 제가 사용한 건 cron 옵션입니다.

@Scheduled(cron = "0 0 0-23 * * *")
	public void autoUpdate() {
		bookService.updateSuccess();
	}

cron 뒤에 오는 *가 각각 의미하는 것은 아래와 같습니다. 미리 예시로 주는 표현법도 있습니다.

제가 설정한 것은 0시부터 23시까지 매시 정각에 실행될 수 있도록 설정했습니다.

 

더 많은 옵션 정보가 필요하시면 아래 링크를 봐주세요.

https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#scheduling

 

설정은 끝났습니다.

메서드 안에 있는 bookService.updateSuccess(); 코드는 제가 매시각 실행할 서비스 로직이라 중요하지 않습니다.

 

 

 

 

https://spring.io/guides/gs/scheduling-tasks/#initial

 

Scheduling Tasks

this guide is designed to get you productive as quickly as possible and using the latest Spring project releases and techniques as recommended by the Spring team

spring.io

 

728x90
728x90

프로젝트를 진행 중에 한 리스트에서 연속된 두 VO를 사용해야 하는데

어떻게 할 수 있을까 고민하다가 생각난 것을 적용해보니 잘 되길래 공유해봅니다.

 

JSTL core의 foreach 태그에는 여러 속성이 있지만 주로 사용하는 방법은 아래와 같습니다.

<c:foreach var="vo" items="${List}">
	<div> ${vo.title} </div>
</c:foreach>

 이렇게 되면 items에 있는 변수가 인덱스 순으로 vo에 담겨서 진행하게 되는데 여기에 속성 몇 개를 추가하면 됩니다.

 

<c:foreach var="vo" items="${List}" varStatus="status" step="2">
	<div> ${vo.title} </div>
    <div> ${List[status.index + 1].title} </div>
</c:foreach>

varStatus 속성으로 설정한 status로 현재 반복문의 인덱스를 받아올 수 있습니다.

step 속성으로는 반복문을 첫 번째 항목부터 시작하여 스텝마다 진행한다는 뜻입니다.

이를 활용하면 세 개씩도 한 반목문 안에 사용할 수 있습니다.

 

예를 들어 아래와 같이 10까지의 숫자가 들어있는 리스트가 있다고 했을 때

String[] list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

아래의 코드를 사용하면 1, 3, 5 ,7, 9만 출력합니다.

<c:foreach var="num" items="${list}" varStatus="status" step="2">
	<div> ${num} </div>
</c:foreach>

여기서 step 속성만 3으로 바꾸면 1, 4, 7, 10이 출력됩니다.

728x90
728x90

https://developers.naver.com/main/

 

NAVER Developers

네이버 오픈 API들을 활용해 개발자들이 다양한 애플리케이션을 개발할 수 있도록 API 가이드와 SDK를 제공합니다. 제공중인 오픈 API에는 네이버 로그인, 검색, 단축URL, 캡차를 비롯 기계번역, 음

developers.naver.com

위 사이트에서 기본적인 서비스 URL과 Callback URL 같은 설정은 맞췄다고 가정하겠습니다.

네이버 로그인 API를 통해서 회원가입까지 구현하기 위해서 기본적인 로직은 크게 5단계로 이루어져 있다고 봅니다.

api 이용 로직


1. 로그인 버튼

API를 사용하려면 우선 로그인 버튼에 내 어플리케이션의 이용 동의를 받는 링크를 설정하는 것입니다.

1. naverlogin.jsp
<%@ page="" import="java.net.URLEncoder" %="">
<%@ page="" import="java.security.SecureRandom" %="">
<%@ page="" import="java.math.BigInteger" %="">
<%@ page="" contentType="text/html;charset=UTF-8" language="java" %="">
<html>
  <head>
    <title>네이버로그인</title>
  </head>
  <body>
  <% String="" clientId="YOUR_CLIENT_ID" ;="" 애플리케이션="" 클라이언트="" 아이디값";="" 
  redirectURI="URLEncoder.encode("YOUR_CALLBACK_URL"," "UTF-8");="" 
  SecureRandom="" random="new" SecureRandom();="" 
  state="new" BigInteger(130,="" random).toString();="" 
  apiURL="https://nid.naver.com/oauth2.0/authorize?response_type=code" 
        +="&client_id=" clientId;="" redirectURI;="" 
        state;="" session.setAttribute("state",="" state);="" %="">
  <a href="<%=apiURL%>"><img height="50" src="http://static.nid.naver.com/oauth/small_g_in.PNG"/></a>
  </body>
</html>

naver developers의 튜토리얼에는 이런식으로 쓰여있습니다.

하지만 저는 클라이언트 아이디나 콜백 주소 그리고 state는 페이지에서 보여주기 싫어서 

로그인 버튼이 보여주는 페이지로 넘어갈 때 컨트롤러에서 객체로 담아 페이지로 값을 넘겨주었습니다.

그러기 위해선 api 관련한 정보를 담을 클래스가 필요해서 그걸 먼저 만드시는 게 좋습니다.

public class SnsVO {
	// naver developers 내 어플리케이션에서 확인
    private String naver_client_id = "본인의 클라이언트 아이디"; 
    // naver developers 내 어플리케이션에서 확인
    private String naver_client_secret = "본인의 클라이언트 비밀번호";
    // 여러개 있을 수 있으니 비워두고 필요할 상황에 set해서 사용가능
    private String naver_redirect_uri = "설정한 리다이렉트 주소";

    private String grant_type;
    private String code;
    private String state;
    private String refresh_token;
    private String access_token;
    private String token_type;
    private String expires_in;
    private String apiURL;
 }

getter/setter를 만드시고 그 후에는 네이버 로그인 관련 메서드를 관리하는 클래스를 만들었습니다.

거기서 a 태그에 설정할 링크를 받아오는 메서드를 우선 만들어 보도록 하겠습니다.

//로그인 관련 vo 객체 profile 객체는 좀더 아래에 나올 예정
SnsVO vo = new SnsProfileVO();

public SnsVO loginApiURL() throws UnsupportedEncodingException {

    vo.setNaver_redirect_uri(URLEncoder.encode(vo.getNaver_redirect_uri(), "UTF-8"));
    SecureRandom random = new SecureRandom();
    vo.setState(new BigInteger(130, random).toString());
    String apiURL = "https://nid.naver.com/oauth2.0/authorize?response_type=code";
    apiURL += "&client_id=" + vo.getNaver_client_id();
    apiURL += "&redirect_uri=" + vo.getNaver_redirect_uri();
    apiURL += "&state=" + vo.getState();

    vo.setApiURL(apiURL);

    return vo;
}

위 코드를 보시면 vo객체에 apiURL을 저장합니다. 

네이버 측에서 리다이렉트 주소를 인코딩해서 보내달라고 요청했으니 잊지 마세요.

loginApiURL 메서드를 통해서 받은 vo 객체를 컨트롤러에서 페이지로 보내는 일만 남았습니다.

spring의 home에 버튼이 있다고 가정하겠습니다.

//컨트롤러
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home(Model model, HttpServletRequest request, HttpSession session) throws UnsupportedEncodingException {

	// 네이버 apiurl을 받아온다.
    SnsVO navervo = naverservice.loginApiURL(); 
    session.setAttribute("state",navervo.getState()); 
    model.addAttribute("navervo", navervo);

    return "home";
}
//HOME.jsp
<a href="${navervo.apiURL}">
	<img height="50" src="http://static.nid.naver.com/oauth/small_g_in.PNG"/>
</a>

튜토리얼과 다르게 뷰페이지에서 간단하게 버튼을 생성했습니다.

 


2. Callback

다음으로 로그인 버튼의 링크 주소와 callback주소에 별다른 이상이 없다면

리다이렉트 주소로 code값과 state값이 반환됩니다.

 

여기서는 리다이렉트 주소를 http://localhost:8090/controller/callback.do으로 가정하겠습니다.

우리는 위에서 sns vo를 만들어 뒀기 때문에 리다이렉트를 받는 해당 컨트롤러 매개변수에 snsvo를

넣어두면 값을 무리 없이 받을 수 있습니다.  

@RequestMapping(value="/callback.do")
public String naverLogin(SnsVO snsvo) throws IOException {
	System.out.println(snsvo.getCode());
    
    return "common/naverCallback";
}

리턴 값은 페이지로 보낼지 다른 서비스 로직을 구현할지에 따라 다르지만

컨트롤러 매개변수에 snsvo만 설정해두면 값을 받을 수 있다는 것을 인지하는 게 중요합니다.

sysout으로 제대로 값을 받아왔는지 확인해도 좋습니다.


3. Access_Token 발급

이제 accseeToken 까지만 발급받으면 로그인 부분은 끝이라고 할 수 있습니다.

새로 페이지나 버튼, 컨트롤러를 만들어서 토큰을 받아올 수도 있으나

여기서는 리다이렉트 컨트롤러에 추가로 구현해보겠습니다.

그러기 위해서는 먼저 메서드를 만들어야 합니다.

네이버 로그인 개발 가이드를 보면 get/post 두 방식으로 요청을 할 수 있고 json으로 응답합니다.

public SnsVO getAccessToken(SnsVO snsVO) {
    String apiURL;
    apiURL = "https://nid.naver.com/oauth2.0/token?grant_type=authorization_code&";
    apiURL += "client_id=" + snsVO.getNaver_client_id();
    apiURL += "&client_secret=" + snsVO.getNaver_client_secret();
    apiURL += "&redirect_uri=" + snsVO.getNaver_redirect_uri();
    apiURL += "&code=" + snsVO.getCode();
    apiURL += "&state=" + snsVO.getState();

    try {
          URL url = new URL(apiURL);
          HttpURLConnection con = (HttpURLConnection)url.openConnection();
          con.setRequestMethod("GET");

          int responseCode = con.getResponseCode();
          BufferedReader br;

          if(responseCode==200) {
            br = new BufferedReader(new InputStreamReader(con.getInputStream()));
          } else {
            br = new BufferedReader(new InputStreamReader(con.getErrorStream()));
          }
          String inputLine;
          while ((inputLine = br.readLine()) != null) {
            ObjectMapper mapper = new ObjectMapper();
            snsVO = mapper.readValue(inputLine, SnsVO.class);
          }
          br.close();
        } catch (Exception e) {
          System.out.println(e);
        }
    return snsVO;			
}

 

getAccessToken(SnsVO snsVO) 메서드의 snsVO는 리다이렉트 주소로 반환된 code값을 사용하기 위해 설정했습니다.

HttpURLConnection 클래스를 통해서 설정한 apiURL 주소에 get방식으로 요청을 보냅니다.

만약 api주소가 정확하여 응답 코드가 200이면 응답 데이터를 버퍼에 담습니다.

하지만 응답 데이터는 json 형식이라 우리가 사용하기 쉽게 맞춰줄 필요가 있습니다.

버퍼에 담긴 값을 ObjectMapper의 readValue 메서드를 이용하면 snsVO 객체에 해당하는 멤버 변수에 

자동으로 맞춰 값을 담아줍니다. 

 

혹시 받아온 json 데이터에서 객체에 없는 값을 무시하고 싶으시면 아래 코드 한 줄 추가하면 됩니다.

mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);

 

그러면 이제 컨트롤러의 코드는 이렇습니다.

@RequestMapping(value="/callback.do")
public String naverLogin(SnsVO snsvo) throws IOException {
	System.out.println(snsvo.getCode());
    
    snsvo = naverService.getAccessToken(snsvo);
    return "common/naverCallback";
}

naverService는 해당 메서드가 있는 클래스입니다.

그리고 그 값을 받는 snsvo는 새로운 객체를 생성해서 받아도 상관없습니다.

 


4. Access_Token으로 유저 프로필 정보 받기

유저 프로필을 받는 이유는 여러 가지가 있을 수 있지만 저는 api를 통해 회원가입 기능도 구현하려고

유저 프로필 정보를 받았습니다.

또한 프로필 정보도 json 형식으로 응답하기 때문에 ObjectMapper로 객체에 저장하는 과정까지 해보겠습니다.

그 이후로 데이터 베이스와 비교해 회원인지 아닌지 확인하는 과정이나 추가 데이터를 받는 것까지는 하지 않겠습니다.

이 과정은 좀 길어서 세 개의 메서드로 나눴습니다.

 

그전에 프로필 정보를 저장할 vo 클래스를 만들어주세요.

public class SnsProfileVO extends SnsVO {

    private String resultcode;
    private String message;
    @JsonProperty
    private JsonNode response; 
    private String age;
    private String mobile;
    private String id;
    private String name;
    private String nickname;
    private String email;
    private String gender;
    private String birthyear;
    private String birthday;
    private String age_range;
}

getter/setter를 생성해주세요.

response는 데이터 타입이 JsonNode인데 응답 데이터의 형식이 아래와 같아서 한 번에 객체에 저장하려고 그렇습니다.

{
    "resultcode": "00",
    "message": "success",
    "response": {
        "email": "openapi@naver.com",
        "nickname": "OpenAPI",
        "profile_image": "https://ssl.pstatic.net/static/pwe/address/nodata_33x33.gif",
        "age": "40-49",
        "gender": "F",
        "id": "32742776",
        "name": "오픈 API",
        "birthday": "10-01"
    }
}

만약 sns 로그인 API를 네이버 하나만 사용하던지 각각 따로 나눠 사용할 거면

데이터 타입을 JsonNode로 하는 게 아니라 내부 클래스를 이용하는 게 훨씬 편합니다.

다음으로는 프로필 정보를 받아오는 메서드입니다.

public SnsProfileVO getUserProfile(SnsVO snsVO) throws IOException {
    SnsProfileVO snsProfile = new SnsProfileVO();

    String token = snsVO.getAccess_token();
    String header = "Bearer " + token;
    String apiURL = "https://openapi.naver.com/v1/nid/me";

    Map<String, String> requestHeaders = new HashMap<String, String>();
    requestHeaders.put("Authorization", header);
	
    // get은 만든 메서드
    snsProfile = get(apiURL, requestHeaders);
    return snsProfile;
}

private static SnsProfileVO get(String apiURL, Map<String,String> requestHeaders) throws IOException {
    URL url = new URL(apiURL);
    HttpURLConnection con = (HttpURLConnection) url.openConnection();

    try {
        con.setRequestMethod("GET");
        for(Map.Entry<String, String> header : requestHeaders.entrySet()) {
        	con.setRequestProperty(header.getKey(), header.getValue());
   		}
    	int responseCode = con.getResponseCode();
        if(responseCode == HttpURLConnection.HTTP_OK) {
        	// readyBody는 아래에 만든 메서드
            return readyBody(con.getInputStream());
        }else {
        	return readyBody(con.getErrorStream());
        }
    }catch(IOException e){
    	throw new RuntimeException("API Error", e);
    }finally {
    	con.disconnect();
    }
}

private static SnsProfileVO readyBody(InputStream body) {
    InputStreamReader streamReader = new InputStreamReader(body);
    SnsProfileVO snsProfile = new SnsProfileVO();

    try(BufferedReader lineReader = new BufferedReader(streamReader)){
        String line;
        while((line = lineReader.readLine()) != null) {
            ObjectMapper mapper = new ObjectMapper();
            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            snsProfile = mapper.readValue(line, SnsProfileVO.class);
            snsProfile = mapper.readValue(snsProfile.getResponse().toString(), SnsProfileVO.class);

        }
    	return snsProfile;
    }catch(IOException e) {
    	throw new RuntimeException("API bufferedReader error", e);
    }
}
for(Map.Entry<String, String> header : requestHeaders.entrySet()) {
        	con.setRequestProperty(header.getKey(), header.getValue());
    }

for문으로 헤더에 토큰 값을 저장합니다. 그리고 요청을 보내 응답을 받습니다.

snsProfile = mapper.readValue(line, SnsProfileVO.class);
snsProfile = mapper.readValue(snsProfile.getResponse().toString(), SnsProfileVO.class);

두 번의 readValue로 프로필 값을 객체에 저장합니다. 

그런데 이렇게 할 경우 객체가 두 개 생성되는 문제를 발견했습니다.

프로필 객체의 response 변수에 담겼던 값만 사용한다면 문제가 없지만

accessToken이나 code를 사용하려면 다른 식으로 저장해야 합니다.

이 문제 때문에 로그인 API를 하나만 사용할 거라면 앞서 이야기한 내부 클래스를 이용하는 게 좋습니다.

여기까지 하면 리다이렉트 주소로 한 번에 프로필 정보까지 받을 수 있습니다.

 

@RequestMapping(value="/callback.do")
public String naverLogin(SnsVO snsvo) throws IOException {
    System.out.println(snsvo.getCode());

    snsvo = naverService.getAccessToken(snsvo);
    SnsProfileVO snsProfile = naverService.getUserProfile(snsvo);
    return "common/naverCallback";
}

이제 프로필 정보까지 받아왔으니 구현하려는 기능에 맞춰 Callback 주소를 설정하면 됩니다.

  • 로그인 
  • 회원가입
  • 토큰 갱신
  • 네이버 이용동의 연결해제 등

원하는 기능에 맞춰 메서드를 사용해 구현할 수 있습니다.

이번에 팀 프로젝트를 하면서 구현해본 API입니다.

부족한 점이 있을 수 있으니 너그러이 봐주시고 고칠 점을 알려주시면 고맙습니다.

카카오나 구글도 비슷하기 때문에 기회가 된다면 다음에 작성해보겠습니다.

728x90
728x90

DI 어노테이션을 사용한 방법

  • @Component

Spring Bean Configuration File을 통해 만든 xml의 context:component-scan 태그

- Namespaces에서 context를 체크한다.

<context:component-scan base-package="패키지이름"/>

xml의 context:component-scan 태그 base-package 속성에 패키지 이름을 적어 넣으면

해당 패키지 안에 있는 클래스를 로드하여 @Component 어노테이션에 있는 클래스를 Bean에 등록한다.

@Component 어노테이션의 경우 클래스에 필드가 없는 경우 사용한다.

 

  • @Configuration

자바 클래스를 설정으로 이용할 수 있다. 클래스 상단에 어노테이션을 적용한다.

클래스 안의 메서드를 @Bean 어노테이션으로 Bean으로 등록하고 메서드명은 xml 설정의  id와 같다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApplicationContext {

	@Bean
	public A methodA() {
		A a = new A();
		a.setName("이병건");
		a.setValue("나이: 41");
		return a;
	}
	@Bean
	public B methodB() {
		B b = new B();
		b.setName("BBB");
		b.setA(methodA()); // setter 주입
		return b;
	}
}

AOP (Aspect Oriented Programming 관점지향)

트랜잭션이나 로깅, 보안과 같이 여러 모듈에서 공통적으로 사용하는 기능을 해당 기능을 분리하여 관리할 수 있다.

Spring Bean Configuration File의 Namespaces에서 aop를 체크한다.

용어

  • Aspect : 공통의 관심사들로 이루어진 모듈
  • ex) 로그에 관련된 모듈을 모아둔 클래스/ 메서드 하나가 모듈 하나로 생각하면 된다.
  • Target : 공통 모듈(Aspect)이 적용될 곳 (클래스 또는 메서드)
  • Advice : 실제 적용될 공통 모듈 하나를 의미
  • Joinpoint : advice가 적용되어야 하는 시점
  • Pointcut : target이 상세화 된 것
<context:component-scan base-package="aopEx01"/>
	<bean id="LoggerAOP01" class="aopEx01.Aspect01"/>
	<aop:config>
		<aop:aspect id="Logger01" ref="LoggerAOP01"> <!-- 참조할 Bean -->
        	<!-- expression으로 적용될 곳을 지정 (aopEx01 패키지의 A 클래스) -->
			<aop:pointcut expression="within(aopEx01.A)" id="pointcutA"/>
			<aop:pointcut expression="within(aopEx01.B)" id="PointcutB"/>
			<aop:pointcut expression="within(aopEx01.C)" id="PointcutC"/>
            
            <!-- 상단에 pointcutA로 지정한 aopEx01 패키지 A 클래스의 
            	loggerAop01 메서드를 around로 지정 -->
			<aop:around method="loggerAop01" pointcut-ref="pointcutA"/>
			<aop:before method="loggerAop02" pointcut-ref="PointcutB"/>
			<!-- 오류가 나더라도 무조건 호출되는 모듈 aop:after -->
			<aop:after method="loggerAop03" pointcut-ref="PointcutC"/> 
			<!-- 핵심 로직이 정상 종료 되었을 때 호출되는 공통 모듈 -->
			<aop:after-returning method="loggerAop03" pointcut-ref="PointcutC"/>
			<!-- 핵심 로직이 오류가 발생하여 종료가 되었을 때 호출되는 공통모듈 -->
			<aop:after-throwing method="loggerAop03" pointcut-ref="PointcutC"/>
		</aop:aspect>
	</aop:config>

ProceedingJoinPoint 클래스

  • .getSignature() 호출되는 메서드에 대한 정보를 구한다.
  • .getTarget() 대상 객체를 구한다.
  • .getName 메서드의 이름을 구한다.
  • .toShortString 메서드의 이름을 반환한다.
  • .toLongString 메서드의 리턴타입, 파라미터 타입을 모두 포함해 반환한다.
  • .Proceed 다음 advice 또는 target의 메서드를 진행한다.

 

 

728x90
728x90

POJO (Plain Old Java Object)

특정 자바 모델이나 기능 프레임워크 등을 따르지 않은 자바 오브젝트를 지칭하는 말로 사용되었다.

자바 언어 사양 외에 어떠한 제한에도 묶이지 않은 자바 오브젝트로 다음과 같은 행동을 해서는 안된다.

- 미리 정의된 클래스의 확장

- 미리 정의된 인터페이스의 구현

- 미리 정의된 애너테이션을 포함

그러나 기술적 어려움이나 기타 이유로 많은 프레임워크들이 미리 정의된 애너테이션의 사용을 요구한다.


DI (Dependency Injection 의존성 주입) 

각각의 계층이나 서비스들 강에 의존성이 존재할 경우 프레임워크가 서로 연결시켜준다.

객체를 직접 생성하는게 아니라 외부에서 생성한 후 주입시켜주기 때문에

모듈 간의 결합도가 낮아지고 유연성이 높아진다.

DI의 종류 

- Setter Injection 

B b = new B();
A a = new A();
a.setB(b); // setter injection

- Construction Injection 생성자 주입

B b = new B();
A a = new A(b); // injection

spring의 DI

- 어노테이션을 활용한 DI

-> 어노테이션이란 @를 붙여 사용하는 특별한 의미가 있는 주석

-> 컴파일러가 특정 오류를 억제하도록 지시하는 것과 같이 프로그램 코드의 일부가 아닌

프로그램에 관한 데이터를 제공, 코드에 정보를 추가하는 정형화된 방법

-. xml을 활용한 DI

-> xml에서 bean으로 class를 등록해서 의존관계를 연결해주는 방식

 


IoC (Inversion of Control 제어 역행)

- 메소드나 객체의 호출 작업을 개발자가 결정하는 것이 아니라 외부에서 결정되는 것을 의미한다.

- 개발자는 프레임워크에 필요한 부품을 개발하고 조립하는 방식의 개발을 하게 되며 조립된 코드의 최종 호출은

개발자에 의해서 제어되는 것이 아니라 프레임워크의 내부에서 결정된 대로 이루어지게 되는데 이러한 현상을 제어의 역전이라고 표현한다.

- 의존적인 객체를 직접생성하거나 제어하는 것이 아니라 특정 객체에 필요한 객체를 외부에서 결정해서 연결해주므로

모듈 간의 결합도가 낮아지고 유연성이 높아진다.

728x90
728x90

클래스 다이어그램을 그릴 수 있는 프로그램을 통해서

미리 각 자바 클래스에서 사용할 변수와 메서드를 작성해봤습니다.

세 게시판이 비슷한 기능들을 하기 때문에 같은 변수와 메서드를 가지고 있습니다.

다른 기능들은 점차 추가하면 될 것 같습니다.

728x90

+ Recent posts