Project/SideProject

[2nd Project] 카카오 로그인 API 구현

ParkYeseul 2024. 10. 1. 18:23

카카오 로그인 이녀석 누가 쉽다고 했어!!!!!
코딩 병아리🐤🐥🐤🐣에게는 아주 어려웠다고요!

도합 8시간 걸렸습니다.

(금방 한 건가) 정확히 3번 엎었습니다.

1. jsp로 해보겠다고 끙끙

2. 스프링 MVC로 해보겠다고 끙끙

3. 마지막 결국 자바스크립트

 

총평: 자바스크립트가 제일 간단하고 깔끔합니다. 다들 굳이 어렵게 하지마시고, 자바스크립트로 하세요

자바스크립트 너 최고야. 😲

 

정리 해보면서 복기를 해보겠습니다. 

 

 


 

리액트로 되어있긴 한데요,, 전체적인 흐름 보기에는 제일 이해가 잘돼서 가져왔습니다.

 

 

 

 

카카오 개발자도구에 가서 애플리케이션 추가하면

 

앱 키가 나온다. 

자바스크립트의 경우 자바스크립트 키로 쓰면 된다.

(개발자도구에서 자세한 순서는 여기 블로그 참고)

 

 

 

 

🧀카카오 로그인 API 파일 


DBCP.java

<데이터베이스 커넥션 풀을 관리하는 클래스>
현재 나는 DBCP를 이용해서 DB를 연결하고 있기 때문에 DBCP를 사용했다. 

이건 각자 개발환경 마다 다를테니까 Pass

 



M_MemberDAO.java

<회원 정보와 관련된 데이터베이스 작업을 수행>

우리 프로젝트에서는 sns로그인과 이메일 일반가입으로 분류되어있다.

그래서 로그인을 하게 되면 member테이블에 모두 올라가고 있는데 type을 줘서 일반 이메일 회원인지, kakao인지 다른 플랫폼인지 알 수 있도록 구분값을 넣어 두었다. 그래서 memberDAO도 필요했다.

public class M_MemberDAO extends DBCP{

	public int insertM_Member(M_MemberDTO dto) {
	    System.out.println("insertM_Member 메소드 호출됨");
	    
	    int result = 0; // 입력 실패 시 결과값
	    int memberIdx = -1; // 반환할 memberIdx 초기화
	    
	    System.out.println("이메일: " + dto.getM_email());
	    
	    // 연결객체를 이용해서 SQL객체를 생성함
	    String sql = "INSERT INTO m_member (m_email, m_password, m_nickname, m_status, m_registration_type) VALUES (?, ?, ?, 'active', 'kakao')";
	    
	    try {
	        conn.setAutoCommit(false); // 오토 커밋 중지
	        
	        // PreparedStatement를 생성할 때 RETURN_GENERATED_KEYS를 지정
	        pstmt = conn.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
	        
	        // SQL객체의 ?에 입력값을 세팅해서 SQL문을 완성시킴
	        pstmt.setString(1, dto.getM_email());
	        pstmt.setString(2, dto.getM_password());
	        pstmt.setString(3, dto.getM_nickname());
	        
	        // SQL문 실행 시킴
	        result = pstmt.executeUpdate();
	        
	        // 생성된 키를 가져옴
	        if (result > 0) {
	            ResultSet generatedKeys = pstmt.getGeneratedKeys();
	            if (generatedKeys.next()) {
	                memberIdx = generatedKeys.getInt(1); // 첫 번째 키(보통 ID)를 가져옵니다.
	            }
	            conn.commit(); // 회원정보 입력 중 예외가 발생하지 않았을 경우 커밋
	        }
	    } catch (Exception e) {
	        System.out.println("회원정보 입력 중 예외 발생: " + e.getMessage());
	        e.printStackTrace(); // 예외의 자세한 스택 트레이스를 출력
	        try {
	            conn.rollback(); // 회원정보 입력 중 예외가 발생하면 이전으로 돌림
	        } catch (SQLException e1) {
	            System.out.println("롤백 실패");
	            e1.printStackTrace();
	        }
	    } finally {
	        try {
	            conn.setAutoCommit(true); // 오토커밋 기능이 되도록 함
	        } catch (SQLException e) {
	            e.printStackTrace();
	        }
	    }

	    return memberIdx; // 생성된 memberIdx 반환
	}

여기서는 회원가입 한 회원 정보를 데이터베이스에 삽입하고, 생성된 회원 ID를 반환한다. 
여기서 특이점은 int memberIdx = -1;

memberIdx회원의 고유 ID로 시퀀스를 이용해 1씩 증가하도록 설정했다. 

그래서 초기값을 -1로 설정한 건, 회원 정보를 데이터베이스에 삽입하지 못했다는 걸 알기 위함이다.


디버깅 하면서 출력해보면 m_idx가 0으로 떠서 DB에 입력이 되지 않고 있었다.

그 문제를 해결하기 위해서 만든 코드이다.




KakaoLoginDAO.java

 

<카카오 로그인 정보를 데이터베이스에 삽입하는 기능을 수행>

public class KakaoLoginDAO extends DBCP {

		public int insertKAKAOLOGIN(KakaoLoginDTO kakaoLoginDTO) {
		System.out.println("insertKAKAOLOGIN 메소드 호출됨");
		
       
        int result = 0;//입력 실패 시 결과값
		
		//연결객체를 이용해서 SQL객체를 생성함
        String sql = "INSERT INTO kakao_login (kakao_id, m_idx, kakao_nickname, create_at) VALUES (?, ?, ?, ?)";
        

		try {
			conn.setAutoCommit(false);//오토 커밋 중지시킴
			
			pstmt = conn.prepareStatement(sql);
			//SQL객체의 ?에 입력값을 세팅해서 SQL문을 완성시킴
			pstmt.setLong(1, kakaoLoginDTO.getKakao_id());
	        pstmt.setInt(2, kakaoLoginDTO.getM_idx());
	        pstmt.setString(3, kakaoLoginDTO.getKakao_nickname());
	        pstmt.setTimestamp(4, kakaoLoginDTO.getCreate_at());
			
	        
			//SQL문 실행시킴
			result = pstmt.executeUpdate();
			
			conn.commit();//회원정보 입력 중 예외가 발생하지 않았을 경우 커밋
			
		} catch (Exception e) {
			System.out.println("회원정보 입력 중 예외 발생" + e.getMessage());
			e.printStackTrace(); // 예외의 자세한 스택 트레이스를 출력
			try {
				conn.rollback();//회원정보 입력 중 예외가 발생하면 이전으로 돌림
			} catch (SQLException e1) {
				System.out.println("롤백 실패");
				e1.printStackTrace();
			}
		} finally {
			try {
				conn.setAutoCommit(true);//오토커밋 기능이 되도록 함
			} catch (SQLException e) { e.printStackTrace();}
		}
		
		return result;
		}
}

위에 memberDAO랑 큰 차이는 없다!

 

 


KakaoService.java

<카카오 API를 통해 사용자 정보를 요청하고, 이 정보를 데이터베이스에 저장>

package service;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

import org.json.JSONObject;

import dao.KakaoLoginDAO;
import dao.M_MemberDAO;
import dto.KakaoLoginDTO;
import dto.M_MemberDTO;

public class KakaoService {
    private static final String USER_INFO_URL = "https://kapi.kakao.com/v2/user/me";
    public void saveUserInfo(String accessToken) {
        HttpURLConnection conn = null;
        BufferedReader br = null;

        try {
            // 카카오 API를 호출하여 사용자 정보 요청
            URL url = new URL(USER_INFO_URL);
            conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setRequestProperty("Authorization", "Bearer " + accessToken);

            int responseCode = conn.getResponseCode();
            if (responseCode == 200) {
                br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                StringBuilder response = new StringBuilder();
                String line;

                while ((line = br.readLine()) != null) {
                    response.append(line);
                }

                // 응답 JSON에서 사용자 정보 추출
                JSONObject userInfoJson = new JSONObject(response.toString());
                Long kakaoId = userInfoJson.getLong("id");
                String nickname = userInfoJson.getJSONObject("properties").getString("nickname");

                // M_MemberDTO 생성 및 데이터 설정
                M_MemberDTO memberDTO = new M_MemberDTO();
                memberDTO.setM_email(kakaoId + "@kakao.com"); // 카카오 ID를 이메일 형식으로 사용
                memberDTO.setM_nickname(nickname);
                memberDTO.setM_registration_type("kakao"); // 가입 방식 설정

                M_MemberDAO memberDAO = new M_MemberDAO();
                KakaoLoginDAO kakaoLoginDAO = new KakaoLoginDAO();

                // 멤버 정보를 DB에 삽입
                System.out.println("MemberDAO에 사용자 정보 삽입 시도 중...");
                int memberIdx = memberDAO.insertM_Member(memberDTO); // m_member에 사용자 정보 삽입
                if (memberIdx != -1) {
                    System.out.println("MemberDAO에 사용자 정보 삽입 성공, KakaoLoginDAO에 정보 삽입 시도 중...");
                  
                    KakaoLoginDTO kakaoLoginDTO = new KakaoLoginDTO();
                    kakaoLoginDTO.setKakao_id(kakaoId);
                    kakaoLoginDTO.setM_idx(memberIdx); // 올바른 memberIdx가 설정되었는지 확인
                    kakaoLoginDTO.setKakao_nickname(nickname);
                    kakaoLoginDTO.setCreate_at(new java.sql.Timestamp(System.currentTimeMillis())); // 현재 시간 설정

                    System.out.println("KakaoLoginDTO 값 확인:");
                    System.out.println("kakao_id: " + kakaoLoginDTO.getKakao_id());
                    System.out.println("m_idx: " + kakaoLoginDTO.getM_idx());
                    System.out.println("kakao_nickname: " + kakaoLoginDTO.getKakao_nickname());
                    System.out.println("create_at: " + kakaoLoginDTO.getCreate_at());

                    // Kakao 로그인 정보를 DB에 삽입
                    int insertResult = kakaoLoginDAO.insertKAKAOLOGIN(kakaoLoginDTO);
                    kakaoLoginDAO.insertKAKAOLOGIN(kakaoLoginDTO);
                    System.out.println("DB에 카카오 사용자 정보가 성공적으로 저장되었습니다.");
                } else {
                    System.err.println("DB에 사용자 정보 저장 실패");
                }
            } else {
                System.err.println("사용자 정보 요청 실패: " + responseCode);
            }
        } catch (Exception e) {
            System.err.println("사용자 정보 저장 중 예외가 발생했습니다.");
            e.printStackTrace();
        } finally {
            try {
                if (br != null) br.close();
                if (conn != null) conn.disconnect();
            } catch (Exception e) {
                System.err.println("자원 해제 중 오류가 발생했습니다.");
                e.printStackTrace();
            }
        }
    }
}

서비스랑 컨트롤러가 제일 어렵다..ㅎ

saveUserInfo 메서드는 액세스 토큰을 사용해 카카오API에 사용자 정보를 요청하고 데이터베이스에 저장하는 역할을 하는 메서드다.

 

JSON 응답 파싱을 하는 이유는 카카오에서 제공하는 응답 데이터는 JSON 형식이다.

Java에서는 JSON 데이터를 직접 사용할 수 없어서 JSONObject 클래스를 사용하여 JSON 문자열을 Java 객체로 변환해야 한다고 한다. 그래서 json자루 파일을 다운 받아서 라이브러리에 추가했다.(최신 버전 하면 될듯)

 


KakaoLoginController.java

<카카오 로그인 요청을 처리하는 서블릿으로

클라이언트로부터 액세스 토큰을 받아 KakaoService를 호출하여 사용자 정보를 저장>

@WebServlet("/login/kakao")
public class KakaoLoginController extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String accessToken = req.getParameter("accessToken");
        if (accessToken == null || accessToken.isEmpty()) {
            System.out.println("액세스 토큰이 전달되지 않았습니다.");
            resp.getWriter().write("액세스 토큰이 전달되지 않았습니다.");
            return;
        }
        System.out.println("액세스 토큰: " + accessToken);

        try {
            // 사용자 정보를 데이터베이스에 저장
            KakaoService kakaoService = new KakaoService();
            kakaoService.saveUserInfo(accessToken);
            resp.sendRedirect(req.getContextPath() + "/index.jsp"); // 로그인 후 메인 페이지로 리다이렉트
        } catch (Exception e) {
            e.printStackTrace();
            resp.getWriter().write("카카오 로그인 중 오류가 발생했습니다.");
        }
    }
}

doGet

메소드는 HTTP GET 요청을 처리하는 메소드

req.getParameter("accessToken")

클라이언트 요청에서 accessToken 파라미터를 가져온다.

 

KakaoService kakaoService = new KakaoService();

카카오 서비스 객체를 생성

kakaoService.saveUserInfo(accessToken);

saveUserInfo 메소드를 호출하여 액세스 토큰을 이용해 카카오 사용자 정보를 저장

 

 

 

 

KakaoLoginDTO.java


M_MemberDTO.java

 

DTO 코드들은 PASS 특이점이 없음!!

 

 

 

 

 

ConfigUtil.java

<애플리케이션의 설정 정보를 관리하는 유틸리티로 데이터베이스 연결 정보와 같은 설정 값을 읽어온다.>

 private static Properties properties = new Properties();
    
    static {
    	
    	String clientId = ConfigUtil.getProperty("kakao.client-id");
    	System.out.println("Client ID: " + clientId);
    	
        try {
            // 파일의 경로를 확인해 보기 위한 디버깅 코드 추가
            URL resourceUrl = ConfigUtil.class.getClassLoader().getResource("config.properties");
            if (resourceUrl == null) {
                System.err.println("설정 파일을 찾을 수 없습니다. 경로를 확인하세요: config.properties");
            } else {
                System.out.println("설정 파일 경로: " + resourceUrl.getPath()); // 설정 파일의 경로 출력

                // 설정 파일 불러오기
                try (InputStream input = ConfigUtil.class.getClassLoader().getResourceAsStream("config.properties")) {
                    if (input != null) {
                        properties.load(input);
                    } else {
                        System.err.println("설정 파일을 불러오는 데 실패했습니다: config.properties");
                    }
                }
            }
        } catch (IOException ex) {
            System.err.println("설정 파일을 로드하는 동안 오류가 발생했습니다.");
            ex.printStackTrace();
        }
    }

    // 설정 파일에서 키에 해당하는 값을 가져오는 메소드
    public static String getProperty(String key) {
        return properties.getProperty(key);
    }
}

 

String clientId = ConfigUtil.getProperty("kakao.client-id")

config.properties 파일에서 kakao.client-id 키에 해당하는 값을 가져와 clientId 변수에 저장

 

 

 

public static String getProperty(String key)

주어진 키에 해당하는 값을 반환하는 공용 정적 메소드

 

return properties.getProperty(key);

Properties 객체에서 키에 해당하는 값을 찾아 반환

 

 

 


KakaoApiUtil.java

<카카오 API와의 통신을 위한 유틸리티로 카카오 API를 호출하고 응답을 처리하는 기능을 한다.>

public static String getAccessToken(String code) {
        String accessToken = "";
        String reqURL = "https://kauth.kakao.com/oauth/token";

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

            // POST 요청 설정
            conn.setRequestMethod("POST");
            conn.setDoOutput(true);
            conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

            // QueryString 형식으로 파라미터 구성
            String params = "grant_type=authorization_code"
                    + "&client_id=자바스크립트 키" // 자바스크립트 키
                    + "&redirect_uri=카카오 개발자 도구에 설정한 Redirect URL" // Redirect URI
                    + "&code=" + code;

            // OutputStream으로 데이터 전송
            try (OutputStream os = conn.getOutputStream()) {
                os.write(params.getBytes());
                os.flush();
            }

            int responseCode = conn.getResponseCode();
            if (responseCode == 200) {
                // 응답이 성공했을 때 액세스 토큰 받기
                try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
                    StringBuilder response = new StringBuilder();
                    String line;
                    while ((line = br.readLine()) != null) {
                        response.append(line);
                    }

                    JSONObject jsonObject = new JSONObject(response.toString());
                    accessToken = jsonObject.getString("access_token");
                }
            } else {
                // 응답이 실패했을 때 상세 메시지 출력
                System.out.println("토큰 요청 실패: " + responseCode);
                try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream()))) {
                    StringBuilder errorResponse = new StringBuilder();
                    String line;
                    while ((line = br.readLine()) != null) {
                        errorResponse.append(line);
                    }
                    System.out.println("실패 내용: " + errorResponse.toString());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();  // 예외 발생 시 스택 트레이스 출력
        }

        return accessToken;
    }
}

 

BufferedReader는 Java에서 제공하는 클래스로, 문자 입력을 효율적으로 읽기 위해 사용

BufferedReader는 기본적으로 입력 스트림을 버퍼링하여, 데이터의 읽기 속도를 높이고, 효율적인 데이터 처리를 가능하게 한다고 함!!



config.properties

<데이터베이스 연결 정보 및 애플리케이션 설정 값을 저장하는 프로퍼티>

kakao.client-id=본인 키
kakao.redirect-uri=본인 URL

이 파일은 경로가 중요해서! 
WEB-INF에 classes폴더를 생성하고 그 안에 넣는 방법

혹은 SRC 안에 그냥 넣는 방법 두 가지가 있는데

WEB-INF를 추천합니닷.

 

joinmain.jsp

 <!-- 카카오 SDK 추가 -->
    <script src="https://developers.kakao.com/sdk/js/kakao.min.js"></script>
    <script type="text/javascript">
        Kakao.init('자바스크립트 키');  // 발급받은 JavaScript 키로 초기화
        console.log("Kakao SDK Initialized.");

        $("#kakao-login-btn").on("click", function() {
            console.log("카카오 로그인 버튼이 클릭되었습니다.");

            Kakao.Auth.login({
                success: function(authObj) {
                    console.log("로그인 성공:", authObj);

                    Kakao.API.request({
                        url: '/v2/user/me',
                        success: function(res) {
                            console.log(res);
                            alert('로그인 성공');
                            // 토큰을 받아 서버에 전달 (여기서는 간단히 Redirect 처리)
                            location.href = 'redirect_url적으면 된다=' + authObj.access_token;
                        },
                        fail: function(err) {
                            console.error("사용자 정보 요청 실패:", err);
                            alert(JSON.stringify(err));
                        }
                    });
                },
                fail: function(err) {
                    console.error("로그인 실패:", err);
                    alert(JSON.stringify(err));
                }
            });
        });
    </script>

 

카카오 로그인 이미지를 버튼 형식으로 해서 만들어놓고 자바스크립트 코드를 추가했다.

자바스크립트로 구현한다면

<script src="https://developers.kakao.com/sdk/js/kakao.min.js"></script>

요건 꼭 들어가야 함!

 

🥩전체적 흐름

사용자 로그인 요청 -> 사용자 정보 요청  -> 데이터베이스에 사용자 정보 저장

 

 

1. 클라이언트에서 카카오 로그인을 시도하면 카카오 서버로부터 액세스 토큰을 받는다.

이 액세스 토큰은 KakaoLoginController에서 처리


2. KakaoLoginController는 KakaoService를 호출하여 액세스 토큰을 사용해 카카오 API에서 사용자 정보 요청
KakaoService는 카카오 API로부터 받은 사용자 정보를 바탕으로

M_MemberDTO를 생성하고, M_MemberDAO를 통해 m_member 테이블에 사용자 정보를 삽입


3. 삽입 후, 생성된 회원 ID(m_idx)를 가져와 KakaoLoginDTO를 생성 이후

KakaoLoginDAO를 통해 카카오 로그인 정보를 kakao_login 테이블에 삽입

 

DBCP로 데이터베이스과 연결

ConfigUtil을 통해 데이터베이스 연결 정보와 같은 설정 값을 읽기

KakaoApiUtil을 통해 카카오 API와의 통신을 처리

 

여기까지가 백엔드의 흐름이었습니다.

 

 

 

이렇게 보면 아주 Smooth하게 흘러간 것 같지만 전.혀 그렇지 않다.

 

🥣KOE101 오류 

 

세 시간 정도 너를 보니까 눈물이 나.

이 오류가 뜨는 근본적인 이유는 

 

유효하지 않은 액세스 토큰

잘못된 API 요청 URL

 

이 두 개일 확률이 아주 높다. 그런데 아무리 봐도 REST API키는..잘 적었고

Redirect_URL도 잘 적었는데 어디서 오류가 생기는 지 찾을 수가 없었다.

개발자 도구에 가서 보니 이런 오류가 적혀있었는데
Request Method:
GET
Status Code:
401 Unauthorized
Remote Address:
27.0.237.15:443
Referrer Policy:
strict-origin-when-cross-origin

 


나는 오류를 해결하지 못했다.

 

이 오류라면 https://development-diary-0h.tistory.com/29 이 블로그를 참고하세요! 

지금 jsp와 내 지식으로는 이건 해결하지 못한다는 판단을 빠르게 내려서 

 

파일을 다 지우고 다시 작성하기 시작했다.

 

그렇게 자바스크립트로! 다시 적었을 때는 참 다양한 오류를 만났는데

자꾸 테이블이 존재하지 않는다고 했다.

디비버에 가서 테이블이 있는지, 권한이 있는지 다 확인을 해봤지만

db쪽 문제는 아니었다.

 

MemberDAO에 사용자 정보 삽입 성공, KakaoLoginDAO에 정보 삽입 시도 중...

DB연결 성공!

삽입 작업 중 오류가 발생하여 롤백합니다.

SQLState: 42000

Error Code: 942

Message: ORA-00942: table or view does not exist

 

 

자 그럼 어떻게 해결을 했는가!

 

2개의 문제가 있었는데 

나는 DB Connection과 DBCP의 두 개의 JDBC가 있었다.

DBCP를 사용하고 있었는데 
내가 파일들을 헷갈려서 DAO에 DBconnection으로 설정을 해두었다.

그래서 안되고 있었고 DBCP로 연결하니까 DB연결을 성공할 수 있었고 다시 테이블을 찾을 수 있었다.

 

거의 2시간 삽질했음.

 

그리고 나서 kakao테이블에 값이 안들어간다. 

디버깅으로 값은 다 끌고 오는 걸 확인 했으나, 값이 안!들!어!감

 

이유는 간단했음. m_idx가 0으로 설정이 되어있었다.

0일 수가 없는데 0으로 설정이 되어있으니 테이블에 데이터가 넘어가지 않았다.

더군다나 나는 멤버 테이블에 1차적으로 넣고 2차로 카카오 테이블에 넣도록 되어있었기에 

멤버DAO에 m_idx를 0으로 해놔서 카카오로 데이터가 넘어가지 않고 있었다.

 

그거를 이제.. 싹 고쳤다 (위에 있음요)

그랬더니 ..

DB에 카카오 사용자 정보가 성공적으로 저장되었습니다.

아주 반가워 ..

 

 

사실 모든게 DAO에서 생긴 오류들이라서 내가 좀만 더 신경 쓰고 집중하면 금방 끝냈을 텐데 흡..

 

장장 몇 시간이야 코드는 기존 수업에서 했던 내용과 별 반 다른 게 없었으나

자잘한 오류를 고치며 .. 시간이 또 흘러버렸다.

 

오류도 아니야 사실 내가 .. 내가 아직 코드 보는 눈이 없는 것 같다.

인정!

 

다음에 네이버랑 구글 로그인 할 때는 덜 헤매고 있으리라 생각하며 ... 이 코드들을 활용해서 네이버오 ㅏ구글도 

해보려고 한다. 

 

자바스크립트로 하세여./ㅎ

 

 

 

 

 

 

참고사이트

https://e1jong.tistory.com/234

https://medium.com/@tellingme/frontend-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%9D%84-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90-by-telling-me-305b3263b374

https://velog.io/@delvering17/JSP-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EC%B9%B4%EC%B9%B4%EC%98%A4%EB%84%A4%EC%9D%B4%EB%B2%84-2.-%EA%B8%B0%EC%B4%88-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-MVC-%EC%84%A4%EC%A0%95