초코딩(chocoding)

[Project - Spring Boot] 소셜 로그인/API - 카카오 : 로그인 유저 정보 db에 저장 / mysql 사용(workbench) / 복합키 지정 / 유저 중복 불가 / @RequiredArgsConstructor 사용 이유 본문

Project

[Project - Spring Boot] 소셜 로그인/API - 카카오 : 로그인 유저 정보 db에 저장 / mysql 사용(workbench) / 복합키 지정 / 유저 중복 불가 / @RequiredArgsConstructor 사용 이유

sweetychocoding 2024. 1. 19. 00:47
728x90

오늘은 저번에 불러온 고객의 정보를 db에 담는 작업을 했다.

 

내가 이 프로젝트를 주된 목적은 소셜 로그인을 위해 각 소셜의 api를 써보기 위함이다.

따라서 db구성도 최소한의 것만 하였고 굳이 회원가입과 로그인을 나누지 않았다.

 

따라서 내가 생각한 최소한의 구현 방식은

 

1. kakao를 통해 불러온 유저 정보 db user table에 담기

2. oauthtype과 oauthtoken을 복합키로 설정하여 oauthId값을 만듬 (user 중복여부를 구분하기 위함)

3. db의 저장된 user의 중복 여부 체크

 

이정도 이다.

 

 

 

 

 

먼저, 내가 구글링 했을 당시에 db에 user 정보를 저장하는 것과

저장한다고 해도 어떤 의도를 가지고 컬럼을 구성했는지에 대한 정보가 부족하였다.

 

너무 너무... 정보가 없어 힘들었기 때문에

내가 구성한 db를 기록할까 한다.

 

우선 앞서 밝혔다시피 나는 최소한의 것을 구성/구현했기 때문에 참고, 방향성 잡기 정도로만 보길 바란다.

 

 

 

.

.

.

 

 

 

내가 카카오에서 가져올 수 있는 정보는

프로필 사진, user 이름, user email 정도였다.

 

어쨌든 중복 처리를 하기 위해서 primary key를 지정해야 하는데

어떻게 지정할까 고민하다가

처음에는 auto increment를 하였다.

 

하지만 auto increment를 해봤자 그 값을 사용하지도 않을텐데

굳이? 라는 생각이 들었고

 

카카오가 기본으로 생성하는 랜덤 형태의 id (oauthtoken)와 user가 가입한 경로(oauthtype)을

복합키로 지정하였다.

 

user table

 

 

 

또한 로그인과 회원가입을 따로 나누지 않고 진행하였기 때문에

회원 중복 체크는 더더욱 필수적이었다.

 

중복체크를 진행하지 않을 시에는

카카오로 로그인하기 라는 버튼을 누를 때마다

같은 user의 정보가 계속해서 담긴다.

 

 

Optional<User>로 검사하여 만약 중복된 OauthId 객체가

있을 경우, 로그인 하게 되고

없을 경우, 회원가입과 로그인이 동시에 이루어지게 된다.

 

 

아래는 위의 모든 설명을 구현한 코드이다.

 

 

 

 

.

.

.

 

 

 

 

KakaoAPI (Service)

package com.cm.personalProject.service;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Optional;

import org.springframework.stereotype.Service;

import com.cm.personalProject.domain.OauthId;
import com.cm.personalProject.entity.User;
import com.cm.personalProject.repository.UserRepository;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Service
@RequiredArgsConstructor
@Log4j2
public class KakaoAPI {

	private final UserRepository repository;

	public String getAccessToken(String authorize_code) {
		String access_Token = "";
		String refresh_Token = "";
		String reqURL = "https://kauth.kakao.com/oauth/token";

		try {
			URL url = new URL(reqURL);
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();

			// POST 요청을 위해 기본값이 false인 setDoOutput을 true로
			conn.setRequestMethod("POST");
			conn.setDoOutput(true);

			// POST 요청에 필요로 요구하는 파라미터 스트림을 통해 전송
			BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
			StringBuilder sb = new StringBuilder();
			sb.append("grant_type=authorization_code");
			sb.append("&client_id=****0b218caf1b7c5c6faed3222c37d3");
			sb.append("&redirect_uri=http://localhost:8080/social/login");
			sb.append("&code=" + authorize_code);
			sb.append("&client_secret=****Gb1FU2Xa3QcDKodnB1zTzEQLzGn5");
			bw.write(sb.toString());
			bw.flush();

			// 결과 코드가 200이라면 성공
			int responseCode = conn.getResponseCode();
			System.out.println("responseCode : " + responseCode);

			// 요청을 통해 얻은 JSON타입의 Response 메세지 읽어오기
			BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
			String line = "";
			String result = "";

			while ((line = br.readLine()) != null) {
				result += line;
			}
			System.out.println("response body : " + result);

			// Gson 라이브러리에 포함된 클래스로 JSON파싱 객체 생성
			JsonParser parser = new JsonParser();
			JsonElement element = parser.parse(result);

			access_Token = element.getAsJsonObject().get("access_token").getAsString();
			refresh_Token = element.getAsJsonObject().get("refresh_token").getAsString();

			System.out.println("access_token : " + access_Token);
			System.out.println("refresh_token : " + refresh_Token);

			br.close();
			bw.close();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return access_Token;
	} // getAccessToken()

	// getUserInfo
	public String getUserInfo(String access_token) {

		String reqUrl = "https://kapi.kakao.com/v2/user/me";
		
		try {
			URL url = new URL(reqUrl);
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			conn.setRequestMethod("POST");

			// 요청에 필요한 Header에 포함될 내용
			conn.setRequestProperty("Authorization", "Bearer " + access_token);
			// conn.setRequestProperty("Content-type",
			// "application/x-www-form-urlencoded;charset=utf-8");

			int responseCode = conn.getResponseCode();
			log.info("[KakaoApi.getUserInfo] responseCode : {}", responseCode);

			BufferedReader br;
			if (responseCode >= 200 && responseCode <= 300) {
				br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
			} else {
				br = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
			}

			String line = "";
			StringBuilder responseSb = new StringBuilder();
			while ((line = br.readLine()) != null) {
				responseSb.append(line);
			}
			String result = responseSb.toString();
			log.info("responseBody = {}", result);

			JsonParser parser = new JsonParser();
			JsonElement element = parser.parse(result);

			JsonObject properties = element.getAsJsonObject().get("properties").getAsJsonObject();
			JsonObject kakao_account = element.getAsJsonObject().get("kakao_account").getAsJsonObject();

			String nickname = properties.getAsJsonObject().get("nickname").getAsString();
			String email = kakao_account.getAsJsonObject().get("email").getAsString();
			String token_id = element.getAsJsonObject().get("id").getAsString();
			
			Optional<User> opt_user = repository.findById(new OauthId("kakao", token_id));

			if (!opt_user.isPresent()) {
				User user = new User();
				user.setUseremail(email);
				user.setUsername(nickname);
				user.setOauthtype("kakao");
				user.setOauthtoken(token_id);
				repository.save(user);
			} else {
				return "fail";
			}
			
			br.close();
			return "success";

		} catch (Exception e) {
			e.printStackTrace();
			return "fail";
		}
	}

}

 

service 구현 시에, @Autowired 대신 @RequiredArgsConstructor을 사용하기를 권장하고 있다.

  • 단일 책임 원칙 관점
  • field에 final 키워드 사용
  • 순환 참조 방지
  • 결합도 낮춰 테스트 용이

하지만 처음에 내가 service에서 UserRepository repository;로 불러오니 @RequiredArgsConstructor을 사용해도 nullpointerException이 발생하였다. 이유를 찾아보니 다음과 같았다.

 

@RequiredArgsConstructor는 final이나 @NonNull로 표시된 필드들만 골라 생성자를 만들기 때문이다.

 

따라서 final 키워드를 사용하여 repository를 불러오니 에러가 해결되었다.

 

 

 

 

 

OauthId (복합키 설정)

package com.cm.personalProject.domain;

import java.io.Serializable;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class OauthId implements Serializable {

	private static final long serialVersionUID = 1L;

	private String oauthtype;
	private String oauthtoken;
}

 

 

 

 

 

LoginController

package com.cm.personalProject.controller;

import java.util.Map;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.cm.personalProject.service.KakaoAPI;

import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;

@Log4j2
@AllArgsConstructor
@RequestMapping(value = "/social")
@Controller
public class LoginController {
	
	private KakaoAPI kakao;
	
	@GetMapping("/loginPage")
	public void getLoginPage() {

	}
	
	@GetMapping("/login")
	public String login(@RequestParam("code") String code) {
		System.out.println(code);
		String access_token = kakao.getAccessToken(code);
        System.out.println("controller access_token : " + access_token);
        
        String userInfo = kakao.getUserInfo(access_token);
        
        if ("success".equals(userInfo)) {
        	return "redirect:/home";
        } else {
        	return "redirect:/social/loginPage";
        }
	}
	
	
	
}

 

 

 

 

 

User

package com.cm.personalProject.entity;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import javax.persistence.Transient;

import com.cm.personalProject.domain.OauthId;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table
@Data
@AllArgsConstructor
@NoArgsConstructor
@IdClass(OauthId.class)
public class User implements Serializable {

	@Transient
	private static final long serialVersionUID = 1L;

	@Id
	private String oauthtype;

	@Id
	private String oauthtoken;

	private String username;
	private String useremail;
}

 

 

 

 

 

UserRepository

package com.cm.personalProject.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.cm.personalProject.domain.OauthId;
import com.cm.personalProject.entity.User;

public interface UserRepository extends JpaRepository<User, OauthId> {

}

 

 

 

 

.

.

.

 

 

 

팀 프로젝트 당시에 소셜 로그인을 구현하지 못해서 아쉬움이 많이 남았는데

개인 프로젝트에서 구현하게 되어 뿌듯하다.

 

이제 구글과 네이버를 통한 소셜 로그인도 구현할 계획이다.

728x90