<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>GiHyeon's Backend Lab</title>
    <link>https://studydeveloper.tistory.com/</link>
    <description>JAVA로 개발할 수 있는 것들에 대해 정리합니다.</description>
    <language>ko</language>
    <pubDate>Mon, 6 Apr 2026 00:28:13 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>gihyeoon</managingEditor>
    <image>
      <title>GiHyeon's Backend Lab</title>
      <url>https://tistory1.daumcdn.net/tistory/5146003/attach/af8efd906a6f4b8ebdaee42b465f6800</url>
      <link>https://studydeveloper.tistory.com</link>
    </image>
    <item>
      <title>PostgreSQL과 Golang DB 연결</title>
      <link>https://studydeveloper.tistory.com/49</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Go + HTMX 프로젝트 진행 도중 DB 연결하는 부분에서 애먹은 부분이 좀 있어 이를 정리할 겸 글을 적습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;* Go언어 프로젝트 기본 설정은 끝났다는 가정 하에 설명하겠습니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 기본적인 PostgreSQL DB 연결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 PostgreSQL 라이브러리를 프로젝트 경로에 설치해야합니다. 아래 명령어를 실행하여 설치합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1767619055776&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;go get github.com/lib/pq&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 DB 연결하는 코드는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1767619126984&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main

import (
	&quot;database/sql&quot;
	&quot;fmt&quot;

	// 이를 선언하지 않으면 unknown driver 오류가 발생한다.
	_ &quot;github.com/lib/pq&quot;
)

const (
	HOST     = &quot;localhost&quot;
	DATABASE = &quot;CREATE한 DB명&quot;
	PORT     = &quot;포트번호&quot;
	USER     = &quot;postgres&quot;
	PASSWORD = &quot;DB 비밀번호&quot;
)

func checkError(err error) {
	if err != nil {
		panic(err)
	}
}

func DbConnect() *sql.DB {
	// 5개의 설정을 다 해줘야 정상적으로 연결이 가능하다.
	var connectionstring string = fmt.Sprintf(&quot;host=%s port=%s user=%s password=%s dbname=%s sslmode=disable&quot;, HOST, PORT, USER, PASSWORD, DATABASE)

	db, err := sql.Open(&quot;postgres&quot;, connectionstring)
	checkError(err)

	err = db.Ping()
	checkError(err)
	fmt.Println(&quot;DB 연결 성공&quot;)

	return db
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 방식은 Microsoft 공식 개발 가이드 문서를 참고해서 작성하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DATABASE에는 pgAdmin에서 추가한 Database명을 작성하고, PORT는 설치 초기에 설정한 포트번호(기본값은 5432), 비밀번호 또한 설치 초기에 본인이 설정한 비밀번호를 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;# 이슈 1&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;_ &quot;github.com/lib/pq&quot; 를 import문에 추가하지 않으면 프로그램 실행 시 &lt;span style=&quot;color: #ee2323;&quot;&gt;unknown driver&lt;/span&gt; 오류가 발생한다. 무조건 추가하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;# 이슈 2&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;connectionstring 변수를 보면 기본적인 DB 설정을 해주는데, 이 때 &lt;span style=&quot;color: #ee2323;&quot;&gt;host, port, user, password, dbname 전부 다 설정&lt;/span&gt;해줘야 연결이 정상적으로 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;# 이슈 3&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드는 main.go에 작성된 것이 아니라 dbconnection.go 라는 파일을 따로 파서 작성한 것이다. 그래서 main.go에서 해당 연결 로직을 호출하고 사용할 수 있게 해야하는데, 그럴려면 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;함수명 첫 글자 대문자 설정, db 객체 반환&lt;/b&gt;&lt;/span&gt;해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 활용해서 메인에서 사용하는 코드는 아래와 같다. (SELECT문)&lt;/p&gt;
&lt;pre id=&quot;code_1767619181755&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main

import (
	&quot;encoding/json&quot;
	&quot;io&quot;
	&quot;log&quot;
	&quot;net/http&quot;
	&quot;text/template&quot;
)

func main() {
	// DB 연결
	con := DbConnect()
	defer con.Close()

	loginHandler := func(w http.ResponseWriter, r *http.Request) {
		body, _ := io.ReadAll(r.Body)
		keyVal := make(map[string]string)
		json.Unmarshal(body, &amp;amp;keyVal)

		userId := keyVal[&quot;userId&quot;]
		userPwd := keyVal[&quot;userPwd&quot;]

		var cnt int
		rows, err := con.Query(&quot;SELECT COUNT(*) as cnt FROM TB_USER WHERE user_id = $1&quot;, userId)

		// 쿼리문 오류 검증 로직
		if err != nil {
			log.Fatal(err)
		}

		for rows.Next() {
			if err := rows.Scan(&amp;amp;cnt); err != nil {
				log.Fatal(err)
			}
		}

		log.Print(userId + &quot; / &quot; + userPwd)
		log.Print(cnt)
	}


	// 매핑
	http.HandleFunc(&quot;/doLogin&quot;, loginHandler)

	log.Fatal(http.ListenAndServe(&quot;:8000&quot;, nil))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에 INSERT, UPDATE, DELETE 사용법은 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1767619927881&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;testHandler := func(w http.ResponseWriter, r *http.Request) {
    ss := &quot;INSERT INTO TB_USER VALUES ($1, $2, $3);&quot;
    _, err := con.Exec(ss, &quot;테스트아이디&quot;, &quot;테스트비번&quot;, &quot;테스트명&quot;)

    if err != nil {
        log.Fatal(err)
    }

    ss = &quot;UPDATE TB_USER SET user_nm = $1 WHERE user_id = $2;&quot;
    _, err = con.Exec(ss, &quot;테스트명2&quot;, &quot;테스트아이디&quot;)

    if err != nil {
        log.Fatal(err)
    }

    ss = &quot;DELETE FROM TB_USER WHERE user_id = $1;&quot;
    _, err = con.Exec(ss, &quot;테스트아이디&quot;)

    if err != nil {
        log.Fatal(err)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot만 하다가 처음으로 Go언어로 백엔드 개발을 해봤는데, 되게 간단명료해서 좋은 것 같다. 기존의 무거운 코드에서 가벼워진 느낌이라 괜찮은 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 처리 관련해서는 아래 사이트를 자주 참고하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/azure/postgresql/connectivity/connect-go?tabs=windows&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://learn.microsoft.com/ko-kr/azure/postgresql/connectivity/connect-go?tabs=windows&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1767620010269&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;빠른 시작: Go를 사용하여 연결&quot; data-og-description=&quot;이 빠른 시작에서는 Azure Database for PostgreSQL 유연한 서버 인스턴스에서 데이터를 연결하고 쿼리하는 데 사용할 수 있는 Go 프로그래밍 언어 샘플을 제공합니다.&quot; data-og-host=&quot;learn.microsoft.com&quot; data-og-source-url=&quot;https://learn.microsoft.com/ko-kr/azure/postgresql/connectivity/connect-go?tabs=windows&quot; data-og-url=&quot;https://learn.microsoft.com/ko-kr/azure/postgresql/connectivity/connect-go&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/btd7zo/dJMb8SXqw60/scUmVv26XZoOiE5au9zKb1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/azure/postgresql/connectivity/connect-go?tabs=windows&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://learn.microsoft.com/ko-kr/azure/postgresql/connectivity/connect-go?tabs=windows&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/btd7zo/dJMb8SXqw60/scUmVv26XZoOiE5au9zKb1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;빠른 시작: Go를 사용하여 연결&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이 빠른 시작에서는 Azure Database for PostgreSQL 유연한 서버 인스턴스에서 데이터를 연결하고 쿼리하는 데 사용할 수 있는 Go 프로그래밍 언어 샘플을 제공합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;learn.microsoft.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java/Golang</category>
      <author>gihyeoon</author>
      <guid isPermaLink="true">https://studydeveloper.tistory.com/49</guid>
      <comments>https://studydeveloper.tistory.com/49#entry49comment</comments>
      <pubDate>Mon, 5 Jan 2026 22:33:43 +0900</pubDate>
    </item>
    <item>
      <title>2025년 3회 정보처리기사 실기 후기</title>
      <link>https://studydeveloper.tistory.com/48</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;2022년에 정보처리기사 산업기사를 취득하고, 2년 이상이 지나서 정보처리기사를 준비하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직장이랑 병행해서 준비를 해야하다보니 6주~7주 정도의 넉넉한 공부 기간을 가졌다. 공부는 2025년도 시나공 정보처리기사 실기 책을 구매해서 기본적인 이론들을 외웠고, 출퇴근 시에는 지하철에서 정보처리기사 실기 요약본 같은 것들을 다운로드받아서 단순 암기하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책을 통해서 공부할 때에는 워낙에 섹션들이 많아서.. 풀면서 복습은 하지 않고 일단 책에 있는 모든 문제들을 다 풀고 나서 그 이후에 이론 복습을 하였다. 공부하고 문제 다 푸는 데에는 4~5주 정도 걸렸던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러고나서 기출문제를 풀기 시작했는데, 생각보다 프로그래밍 문제가 많았다..? 비율이 거의 프로그래밍, 이론 각각 6:4 정도 되었던 것 같은데, 보자마자 확실히 전공자들에게는 쉬울 것 같고 비전공자들에게는 많이 어려울 수도 있는.. 문제들이 많았다. (특히 포인터, 재귀 함수 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 책 마무리 하고 남은 1~2주 기간 동안에는 계~속 책 보면서 복습하고, 다른 실기 이론들 정리한 블로그들도 참고해보고 하면서 공부를 마무리함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대망의 시험날 11월 8일.. 실기 시험은 처음이었어서 (기능사랑 산업기사는 대회 수상으로 면제..) 잘 몰랐는데, 페이퍼로 시험을 진행하다보니 &lt;b&gt;검정색 펜&lt;/b&gt;이 필요하였다. 아예 빈손으로 갔는데 ㅋㅋㅋㅋ.. 다행히 관계자분에게 말씀드려서 받긴했는데.. 실기 시험 갈 때는 꼭 검정색 팬을 챙기시길&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험 내용으로 다시 얘기하자면 일단 전공자 입장에서는 꽤 쉬웠다. 아무래도 지난 1, 2회차의 난이도가 좀 높아서 그랬는지 조절을 한 것으로 보였다. 프로그래밍 쪽에서도 어려운 문제는 포인터 정도였고, 이론은 좀 충격적이었던게 있는데, 이게 책에서는 정리가 잘 안되었던 문제였다. &lt;b&gt;Hamming 코드, FEC, BEC, Parity 비트, CRC&lt;/b&gt; 문제인데, 책에는 기출문제로만 정의되어있었고 따로 내용으로는 정리가 되어있지 않은 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 기출문제랑 아예 똑같이 문제를 냈어서 망정이지.. 보자마자 이게 나온다고..? 싶었던 문제였다. 그리고 예전 기출문제에 있었던 &lt;b&gt;테스트 조건, 데이터, 예상 결과에 대한 문제&lt;/b&gt;도 나왔었는데, 이를 보고 20문제밖에 안되는 실기 시험이라고 하더라도 &lt;b&gt;기출문제 복습이 참 중요&lt;/b&gt;하겠구나 라는 것을 느꼈다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 20문제 중에 14문제 맞아서 (가채점) 합격한 것 같다..? (아마도..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>자격증</category>
      <author>gihyeoon</author>
      <guid isPermaLink="true">https://studydeveloper.tistory.com/48</guid>
      <comments>https://studydeveloper.tistory.com/48#entry48comment</comments>
      <pubDate>Tue, 11 Nov 2025 21:34:56 +0900</pubDate>
    </item>
    <item>
      <title>Golang 개발환경 구축 (VS Code)</title>
      <link>https://studydeveloper.tistory.com/47</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Golang 다운로드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://go.dev/dl/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://go.dev/dl/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760181005577&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;All releases - The Go Programming Language&quot; data-og-description=&quot;&quot; data-og-host=&quot;go.dev&quot; data-og-source-url=&quot;https://go.dev/dl/&quot; data-og-url=&quot;https://go.dev/dl/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dIvvnz/hyZKlju7Xi/VXkPitfhKrafIfX9saLq11/img.jpg?width=300&amp;amp;height=313&amp;amp;face=0_0_300_313&quot;&gt;&lt;a href=&quot;https://go.dev/dl/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://go.dev/dl/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dIvvnz/hyZKlju7Xi/VXkPitfhKrafIfX9saLq11/img.jpg?width=300&amp;amp;height=313&amp;amp;face=0_0_300_313');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;All releases - The Go Programming Language&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;go.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 사이트를 접속하면 Golang 공식 홈페이지의 Go 릴리즈 버전들을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 &lt;b&gt;&quot;Featured downloads&quot;&lt;/b&gt; 섹션에서 본인의 OS에 맞게 파일을 다운로드하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VS Code 환경 구축&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VS Code가 기본적으로 다운로드되어 있다는 전제하에 진행하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확장의 경우 크게 다운로드받을 것은 없고, &quot;Go&quot; 검색 후 가장 상단에 뜨는 플러그인을 다운로드하면 끝이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목없음1.png&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jc90c/btsQ695xlge/FMK3JvzzHJjsSWaNhSt4KK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jc90c/btsQ695xlge/FMK3JvzzHJjsSWaNhSt4KK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jc90c/btsQ695xlge/FMK3JvzzHJjsSWaNhSt4KK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJc90c%2FbtsQ695xlge%2FFMK3JvzzHJjsSWaNhSt4KK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1914&quot; height=&quot;1024&quot; data-filename=&quot;제목없음1.png&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 하면 환경 구축은 완료되었고, 실제로 Go 파일이 실행되는지도 확인해줘야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 현재 폴더에서 Go 모듈을 초기화해 주는 작업을 진행해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VS Code에서 터미널을 킨 뒤에 아래의 명령어를 실행해 준다.&lt;/p&gt;
&lt;pre id=&quot;code_1760181513720&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;go mod init example.com/test&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;*&lt;/span&gt; example.com/test&lt;/span&gt; : 실제로 프로젝트를 진행할 때에는 본인의 GitHub 저장소의 주소로 지정해줘야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, &lt;b&gt;&quot;github.com/사용자명/저장소명&quot;&lt;/b&gt; 과 같이 적어야 한다는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 되면 나중에 다른 사람이 본인의 모듈을 import 할 때 해당 github 주소를 적어서 import할 수가 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 명령어를 실행하게 되면 프로젝트 root 쪽에 go.mod라는 파일이 생성된다. 그렇게 되면 모듈 초기화 작업은 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 이제 실제로 실행할 Go 파일을 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1760181374333&quot; class=&quot;go&quot; data-ke-language=&quot;go&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func main() {
	hello := &quot;Hello World&quot;
	fmt.Println(hello)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;test.go&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 작성하고서 터미널에 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&quot;go run test.go&quot; &lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;라고&lt;/span&gt;&lt;/span&gt; 작성 후 실행하면 &lt;b&gt;Hello World&lt;/b&gt; 가 터미널에 정상적으로 보이는&amp;nbsp;것을 확인할 수 있다.&lt;/p&gt;</description>
      <category>Java/Golang</category>
      <author>gihyeoon</author>
      <guid isPermaLink="true">https://studydeveloper.tistory.com/47</guid>
      <comments>https://studydeveloper.tistory.com/47#entry47comment</comments>
      <pubDate>Sat, 11 Oct 2025 20:24:06 +0900</pubDate>
    </item>
    <item>
      <title>[Android 공부] Spring Boot와 연계를 통한 회원가입 구현(3)</title>
      <link>https://studydeveloper.tistory.com/45</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;저번에 이메일 중복 체크를 하였고, 이번에는 실제 회원가입을 구현해볼 겁니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;백엔드 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 회원가입 요청/응답 DTO 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청은 이메일(username), 비밀번호, 이름, 나이, 전화번호, 역할(유저 or 어드민) 값들을 정의하였고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답은 회원가입 성공 여부, 메시지를 정의하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748869074289&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class RegisterRequest {

	private String username;
	
	private String password;
	
	private String name;
	
	private int age;
	
	private String phoneNum;
	
	private String role;
	
	public String getUsername() {
		return username;
	}
	
	public String getPassword() {
		return password;
	}
	
	public String getName() {
		return name;
	}
	
	public int getAge() {
		return age;
	}
	
	public String getPhoneNum() {
		return phoneNum;
	}
	
	public String getRole() {
		return role;
	}
	
	public void setUsername(String username) {
		this.username = username;
	}
	
	public void setPassword(String password) {
		this.password = password;
	}
	
	public void setName(String name) {
		this.name = name;
	}
	
	public void setAge(int age) {
		this.age = age;
	}
	
	public void setPhoneNum(String phoneNum) {
		this.phoneNum = phoneNum;
	}
	
	public void setRole(String role) {
		this.role = role;
	}
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1748869084388&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class RegisterResponse {

	private boolean isSuccess;
	
	private String message;
	
	public RegisterResponse(boolean isSuccess, String message) {
		this.isSuccess = isSuccess;
		this.message = message;
	}
	
	public String getMessage() {
		return message;
	}

	public boolean isSuccess() {
		return isSuccess;
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;2. 회원가입을 처리할 Controller 메서드 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;모든 프로그램에서 비밀번호라는 값은 RAW 값을 DB에 저장해서는 절대 안됩니다!!&lt;/b&gt;&lt;/span&gt; 비밀번호라는 값이 넘어가게 되면 해당 회원의 개인정보들이 털릴 수 있기 때문에 최소한 비밀번호라도 암호화해서 넣는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암호화하는 로직은 Spring Security 의존성을 추가했다면 사용할 수 있는 &lt;b&gt;BCrpytPasswordEncoder&lt;/b&gt;를 사용하여 진행해주시면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748869263296&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;/register&quot;)
public ResponseEntity&amp;lt;?&amp;gt; register(@RequestBody RegisterRequest request) {
    // 비밀번호 암호화 로직
    String inputPassword = request.getPassword();
    String encodedPassword = passwordEncoder.encode(inputPassword);

    request.setPassword(encodedPassword);

    try {
        authService.register(request);
        return ResponseEntity.ok(new RegisterResponse(true, &quot;회원가입 성공&quot;));
    } catch (Exception e) {
        e.printStackTrace();
        return ResponseEntity.ok(new RegisterResponse(false, &quot;회원가입 실패&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 회원가입을 처리할 Service 메서드 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청DTO를 파라미터로 받고, &lt;b&gt;User 엔티티 객체를 생성&lt;/b&gt;한 후 &lt;b&gt;JPA에서 기본 제공하는 save(entity) 메서드를 사용&lt;/b&gt;해 DB에 저장합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748869172482&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void register(RegisterRequest request) {
    User user = new User(request.getUsername(), request.getPassword(), request.getName(), request.getAge(),
            request.getPhoneNum(), request.getRole());

    userRepository.save(user);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프론트 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 회원가입 요청/응답 DTO 추가&lt;/h3&gt;
&lt;pre id=&quot;code_1748869635817&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class RegisterRequest {

    private String username;
    private String password;
    private String name;
    private int age;
    private String phoneNum;

    private String role;

    public RegisterRequest() {

    }

    public RegisterRequest(String username, String password, String name, int age, String phoneNum, String role) {
        this.username = username;
        this.password = password;
        this.name = name;
        this.age = age;
        this.phoneNum = phoneNum;
        this.role = role;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getPhoneNum() {
        return phoneNum;
    }

    public String getRole() {
        return role;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1748869671263&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class RegisterResponse {
    public boolean isSuccess;
    public String message;

    public boolean isSuccess() {
        return isSuccess;
    }

    public String getMessage() {
        return message;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. RegisterActivity에 회원가입 버튼 클릭 이벤트 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유효성 확인은 기본적으로 진행하고, 회원가입이 정상적으로 되었다면, 로그인 페이지로 자동 이동할 수 있도록 하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748869482451&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// &quot;회원가입&quot; 버튼 클릭 시 로직
registerButton.setOnClickListener(e2 -&amp;gt; {
    emailId = emailEditText.getText().toString().trim();
    selectedEmail = emailSpinner.getSelectedItem().toString();
    pwd = pwdEditText.getText().toString().trim();
    name = nameEditText.getText().toString().trim();
    age = ageEditText.getText().toString().trim();
    phoneNum = phoneNumEditText.getText().toString().trim();

    // 이메일 중복 확인을 안 했을 경우
    if (!isCheckEmail) {
        Toast.makeText(getApplicationContext(), &quot;이메일 중복 확인을 진행해주세요.&quot;, Toast.LENGTH_SHORT).show();
        return;
    }

    // 비밀번호가 8자 미만일 경우
    if (pwd.length() &amp;lt; 8) {
        Toast.makeText(getApplicationContext(), &quot;비밀번호는 8자 이상으로 입력해주세요.&quot;, Toast.LENGTH_SHORT).show();
        return;
    }

    // 이름 입력란이 비어있을 경우
    if (name.isEmpty()) {
        Toast.makeText(getApplicationContext(), &quot;이름을 입력해주세요.&quot;, Toast.LENGTH_SHORT).show();
        return;
    }

    // 나이 값이 1 아래일 경우
    if (age.isEmpty() || Integer.parseInt(age) &amp;lt; 1) {
        Toast.makeText(getApplicationContext(), &quot;나이를 입력해주세요.&quot;, Toast.LENGTH_SHORT).show();
        return;
    }

    // 전화번호가 11자가 아닐 경우 =&amp;gt; 하이픈은 제외하고 숫자만 입력
    if (phoneNum.length() != 11) {
        Toast.makeText(getApplicationContext(), &quot;전화번호는 하이픈 제외 11자로 입력해주세요.&quot;, Toast.LENGTH_SHORT).show();
        return;
    }

    // &quot;기타(직접입력)&quot; 이 선택되어있으면 이메일아이디 입력란으로 이메일 값 할당, 그게 아닐 경우 콤보박스 값까지 받아서 이메일 값 할당
    if (selectedEmail.equals(&quot;기타(직접입력)&quot;)) {
        fullEmailVal = emailId;
    } else {
        fullEmailVal = emailId + &quot;@&quot; + selectedEmail;
    }

    // 회원가입 진행 시 요청보낼 request 정의
    RegisterRequest request = new RegisterRequest(fullEmailVal, pwd, name, Integer.parseInt(age), phoneNum, &quot;USER&quot;);

    // 회원가입 진행
    apiService.register(request).enqueue(new Callback&amp;lt;RegisterResponse&amp;gt;() {
        @Override
        public void onResponse(@NonNull Call&amp;lt;RegisterResponse&amp;gt; call, @NonNull Response&amp;lt;RegisterResponse&amp;gt; response) {
            if (response.isSuccessful() &amp;amp;&amp;amp; response.body() != null) {
                Toast.makeText(getApplicationContext(), &quot;회원가입이 완료되었습니다.&quot;, Toast.LENGTH_SHORT).show();
                Intent intent = new Intent(getApplicationContext(), LoginActivity.class);
                startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(RegisterActivity.this).toBundle());
            } else {
                Toast.makeText(getApplicationContext(), &quot;회원가입 실패. 오류 발생&quot;, Toast.LENGTH_SHORT).show();
            }
        }

        @Override
        public void onFailure(@NonNull Call&amp;lt;RegisterResponse&amp;gt; call, @NonNull Throwable t) {
            Log.e(&quot;Retrofit&quot;, &quot;Login Failed: &quot; + t.getMessage(), t);
            Toast.makeText(getApplicationContext(), &quot;서버 통신 오류&quot;, Toast.LENGTH_SHORT).show();
        }
    });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. ApiService POST 요청 메서드 추가&lt;/h3&gt;
&lt;pre id=&quot;code_1748869709372&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@POST(&quot;/api/android/register&quot;)
Call&amp;lt;RegisterResponse&amp;gt; register(@Body RegisterRequest request);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java/Android</category>
      <category>Android</category>
      <category>android 회원가입</category>
      <category>spring boot</category>
      <category>모바일앱개발</category>
      <author>gihyeoon</author>
      <guid isPermaLink="true">https://studydeveloper.tistory.com/45</guid>
      <comments>https://studydeveloper.tistory.com/45#entry45comment</comments>
      <pubDate>Mon, 2 Jun 2025 22:09:04 +0900</pubDate>
    </item>
    <item>
      <title>[Android 공부] Spring Boot와 연계를 통한 회원가입 구현 - 이메일 중복 체크(2)</title>
      <link>https://studydeveloper.tistory.com/44</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;저번에 로그인을 구현해봤는데, 회원가입도 다를 게 없었습니다. 오히려 더 간단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 시에는 이메일, 비밀번호, 이름, 나이, 전화번호를 입력받아서 DB에 저장하고, 이메일은 중복확인을 통해 이미 DB에 존재하는 이메일인지 체크할 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;백엔드 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 이메일 중복 체크 시 사용할 요청/응답 DTO 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일 중복 체크 시 사용할 DTO를 추가할 건데, 이름은 &lt;b&gt;EmailCheckRequest, EmailCheckResponse&lt;/b&gt;로 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트에서 &lt;span style=&quot;color: #ee2323;&quot;&gt;중복 체크를 요청할 때 이메일 값만 넘길 것&lt;/span&gt;이므로, username(변수명은 security랑 혼동하지 않기 위해 username으로 하였음)만 정의했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;응답 때는 존재 여부만 전달&lt;/span&gt;해주면 되기 때문에 exists Boolean 변수를 정의했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748867007085&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EmailCheckRequest {

	public String username;
	
	public String getUsername() {
		return username;
	}
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1748867014861&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EmailCheckResponse {

	private boolean exists;
	
	public EmailCheckResponse(boolean exists) {
		this.exists = exists;
	}
	
	public boolean isExists() {
		return exists;
	}
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 이메일 중복 체크를 처리할 Controller 메서드 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 있던 AuthController를 사용할 것이고, &lt;b&gt;요청받은 이메일 값을 DB에서 조회&lt;/b&gt;하여 &lt;span style=&quot;color: #006dd7;&quot;&gt;존재할 경우 true&lt;/span&gt;를, &lt;span style=&quot;color: #006dd7;&quot;&gt;존재하지 않을 경우 false를 반환&lt;/span&gt;합니다. URL 값은 원하시는대로 하셔도 됩니다. ex) validate-email, email-check 등등..&lt;/p&gt;
&lt;pre id=&quot;code_1748867209972&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;/check-email&quot;)
public ResponseEntity&amp;lt;?&amp;gt; checkEmail(@RequestBody EmailCheckRequest request) {
    boolean exists = authService.checkEmail(request.getUsername());
    return ResponseEntity.ok(new EmailCheckResponse(exists));
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 이메일 중복 체크를 처리할 Service 메서드 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 SQL의 &lt;b&gt;COUNT를 통해서 존재 여부를 파악&lt;/b&gt;할 것이므로 &lt;span style=&quot;color: #006dd7;&quot;&gt;0보다 크면(존재하면) true, 아니면 false를 반환&lt;/span&gt;합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748867355422&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public boolean checkEmail(String username) {
    return userRepository.countByEmail(username) &amp;gt; 0 ? true : false;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 실제로 이메일 중복 여부를 판단하는 Repository 메서드 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 JPA를 사용하는데에도 불구하고 NativeQuery를 자주 사용하는데, 제가 직접 SQL을 짜는거를 더 좋아하기도 하고, 나중에 수정할 때 명명 규칙으로 판단하는게 아니라 쿼리를 직접 보면서 판단할 수 있어서 편해서 애용하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리는 단순하게 요청받은 이메일을 조건으로 user 테이블에 몇 개가 조회되는지 확인하는 쿼리입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748867364573&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NativeQuery(&quot;SELECT COUNT(*) FROM user WHERE email = ?1&quot;)
int countByEmail(String email);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프론트 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 회원가입 화면 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일, 비밀번호, 이름, 나이, 전화번호 입력란이 존재하고, 회원가입 버튼, 로그인으로 이동하는 버튼, 메인으로 이동하는 버튼 총 3개가 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 회원가입 들어갔다가 갇히면 안되니깐 ㅎㅎ 다른 페이지로 나올 수 있도록 버튼을 추가했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직은 UI가 단순한데, 기능들이 전부 구현 완료되면 예쁘게.. 그럴듯하게.. 개선해보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748867798787&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;ScrollView xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    android:padding=&quot;24dp&quot;&amp;gt;

    &amp;lt;LinearLayout
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:orientation=&quot;vertical&quot;
        android:gravity=&quot;center_horizontal&quot;&amp;gt;

        &amp;lt;!-- 이메일 --&amp;gt;
        &amp;lt;LinearLayout
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:orientation=&quot;horizontal&quot;
            android:gravity=&quot;center_vertical&quot;
            android:layout_marginBottom=&quot;8dp&quot;&amp;gt;

            &amp;lt;EditText
                android:id=&quot;@+id/editEmailId&quot;
                android:layout_width=&quot;0dp&quot;
                android:layout_weight=&quot;2&quot;
                android:layout_height=&quot;wrap_content&quot;
                android:hint=&quot;이메일&quot; /&amp;gt;

            &amp;lt;TextView
                android:layout_width=&quot;wrap_content&quot;
                android:layout_height=&quot;wrap_content&quot;
                android:text=&quot;\@&quot;
                android:textSize=&quot;18sp&quot;
                android:paddingHorizontal=&quot;4dp&quot; /&amp;gt;

            &amp;lt;Spinner
                android:id=&quot;@+id/spinnerEmailDomain&quot;
                android:layout_width=&quot;0dp&quot;
                android:layout_weight=&quot;2&quot;
                android:layout_height=&quot;wrap_content&quot; /&amp;gt;

            &amp;lt;Button
                android:id=&quot;@+id/btn_check_email&quot;
                android:layout_width=&quot;wrap_content&quot;
                android:layout_height=&quot;wrap_content&quot;
                android:text=&quot;중복확인&quot;
                android:layout_marginStart=&quot;8dp&quot; /&amp;gt;
        &amp;lt;/LinearLayout&amp;gt;

        &amp;lt;!-- 비밀번호 --&amp;gt;
        &amp;lt;EditText
            android:id=&quot;@+id/editPassword&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:hint=&quot;비밀번호&quot;
            android:inputType=&quot;textPassword&quot; /&amp;gt;

        &amp;lt;!-- 이름 --&amp;gt;
        &amp;lt;EditText
            android:id=&quot;@+id/editName&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:hint=&quot;이름&quot;
            android:inputType=&quot;textPersonName&quot;/&amp;gt;

        &amp;lt;!-- 나이 --&amp;gt;
        &amp;lt;EditText
            android:id=&quot;@+id/editAge&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:hint=&quot;나이&quot;
            android:inputType=&quot;number&quot; /&amp;gt;

        &amp;lt;!-- 전화번호 --&amp;gt;
        &amp;lt;EditText
            android:id=&quot;@+id/editPhone&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:hint=&quot;전화번호&quot;
            android:inputType=&quot;phone&quot;/&amp;gt;

        &amp;lt;!-- 버튼들 --&amp;gt;
        &amp;lt;Button
            android:id=&quot;@+id/btnSignup&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:text=&quot;회원가입&quot; /&amp;gt;

        &amp;lt;Button
            android:id=&quot;@+id/btnGoLogin&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:text=&quot;로그인으로 돌아가기&quot;
            android:layout_marginTop=&quot;12dp&quot;/&amp;gt;

        &amp;lt;Button
            android:id=&quot;@+id/btnGoMain&quot;
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:text=&quot;메인으로 이동&quot;
            android:layout_marginTop=&quot;12dp&quot;/&amp;gt;

    &amp;lt;/LinearLayout&amp;gt;
&amp;lt;/ScrollView&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이렇게 view를 추가하면 AndroidManifest.xml 파일에 추가로 정의하셔야합니다!! 안 하시면 안드로이드에서 해당 view 페이지를 불러오지를 못해요.&lt;/p&gt;
&lt;pre id=&quot;code_1748868366865&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;activity android:name=&quot;.activity.RegisterActivity&quot;&amp;gt;
&amp;lt;/activity&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 이메일 중복 체크 요청/응답 DTO 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드에서 설정했던 것처럼 &lt;b&gt;요청&lt;/b&gt;은 &lt;span style=&quot;color: #ee2323;&quot;&gt;이메일 값만 담을 username만 정의&lt;/span&gt;하고, &lt;b&gt;응답&lt;/b&gt;은 e&lt;span style=&quot;color: #ee2323;&quot;&gt;xists Boolean 변수를 정의&lt;/span&gt;했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748868031192&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EmailCheckRequest {

    private String username;

    public EmailCheckRequest() {}

    public EmailCheckRequest(String username) {
        this.username = username;
    }

    public String getUsername() {
        return username;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1748868048325&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EmailCheckResponse {

    public boolean exists;

    public EmailCheckResponse() {
    }

    public boolean isExists() {
        return exists;
    }
    public void setExists(boolean exists) {
        this.exists = exists;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. RetrofitClient 메서드 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조를 보시면 단순하게 서버 통신을 설정하는 메서드인데, 이를 추가한 이유는 추후에 회원가입이나, 메인 페이지, 회원정보 수정 등등..&lt;b&gt; 또 다른 서버와의 통신 때를 대비하여 편하게 쓰기 위해 전역으로 만들어놓은 메서드&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보시면 통신 간에 &lt;span style=&quot;color: #006dd7;&quot;&gt;요청/응답을 로그로 출력하는 것도 설정&lt;/span&gt;하였는데, 필수는 아니고 개발하면서 어디가 문제인가 확인할 때 편해서 추가해놨습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748868144346&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static ApiService getApiService() {
    HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
    logging.setLevel(HttpLoggingInterceptor.Level.BODY);

    OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(logging)
            .build();

    if (retrofit == null) {
        retrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(client)
                .addConverterFactory(GsonConverterFactory.create()) // JSON 데이터를 자동으로 객체 변환
                .build();
    }

    return retrofit.create(ApiService.class);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. RegisterActivity 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일 중복 체크를 하기 전에, 이메일이 입력되었는지, 이메일 아이템 선택박스를 선택했는지 등등 검사를 한 뒤에 Spring Boot랑 통신해서 처리하도록 구현하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;span style=&quot;color: #ee2323;&quot;&gt;중복 체크를 하면 isCheckEmail 변수를 true로 설정&lt;/span&gt;해주는 것을 확인할 수 있는데, 이는 &lt;b&gt;회원가입&lt;/b&gt;을 구현할 때 &lt;span style=&quot;color: #ee2323;&quot;&gt;이메일 중복 체크 여부를 판단&lt;/span&gt;하기 위해 미리 추가해놓은 변수입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748868467712&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import android.app.ActivityOptions;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import com.lgh.flipmarketandroid.R;
import com.lgh.flipmarketandroid.config.ApiService;
import com.lgh.flipmarketandroid.config.RetrofitClient;
import com.lgh.flipmarketandroid.dto.user.EmailCheckRequest;
import com.lgh.flipmarketandroid.dto.user.EmailCheckResponse;
import com.lgh.flipmarketandroid.dto.user.RegisterRequest;
import com.lgh.flipmarketandroid.dto.user.RegisterResponse;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class RegisterActivity extends AppCompatActivity {

    private String emailId;
    private String selectedEmail;
    private String pwd;
    private String name;
    private String age;
    private String phoneNum;
    private String fullEmailVal = &quot;&quot;;
    private boolean isCheckEmail = false;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_register);

        EditText emailEditText = findViewById(R.id.editEmailId);
        Spinner emailSpinner = findViewById(R.id.spinnerEmailDomain);
        Button checkEmailButton = findViewById(R.id.btn_check_email);
        EditText pwdEditText = findViewById(R.id.editPassword);
        EditText nameEditText = findViewById(R.id.editName);
        EditText ageEditText = findViewById(R.id.editAge);
        EditText phoneNumEditText = findViewById(R.id.editPhone);
        Button registerButton = findViewById(R.id.btnSignup);
        Button loginButton = findViewById(R.id.btnGoLogin);
        Button mainButton = findViewById(R.id.btnGoMain);

        // 이메일 선택박스 초기화
        String[] items = { &quot;선택&quot;, &quot;naver.com&quot;, &quot;daum.net&quot;, &quot;gmail.com&quot;, &quot;nate.com&quot;, &quot;기타(직접입력)&quot; };
        ArrayAdapter&amp;lt;String&amp;gt; adapter = new ArrayAdapter&amp;lt;&amp;gt;(
                this,
                android.R.layout.simple_spinner_item,
                items
        );
        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        emailSpinner.setAdapter(adapter);

        // 서버 통신 요청
        ApiService apiService = RetrofitClient.getApiService();

        // &quot;중복확인&quot; 버튼 클릭 시 로직
        checkEmailButton.setOnClickListener(e1 -&amp;gt; {
            emailId = emailEditText.getText().toString().trim();
            selectedEmail = emailSpinner.getSelectedItem().toString();

            // 이메일아이디 입력란이 비어있을 경우
            if (emailId.isEmpty()) {
                Toast.makeText(getApplicationContext(), &quot;이메일을 입력해주세요.&quot;, Toast.LENGTH_SHORT).show();
                return;
            }

            // 이메일 선택 박스가 &quot;선택&quot;으로 선택되어있을 경우
            if (selectedEmail.equals(&quot;선택&quot;)) {
                Toast.makeText(getApplicationContext(), &quot;이메일 양식을 선택해주세요.&quot;, Toast.LENGTH_SHORT).show();
                return;
            }

            // 이메일 선택 박스에 &quot;기타(직접입력)&quot;이 선택되어있고, 이메일아이디의 형식이 이메일 정규식에 맞지 않을 경우
            if (selectedEmail.equals(&quot;기타(직접입력)&quot;) &amp;amp;&amp;amp; !emailId.matches(&quot;^[a-zA-Z0-9_+&amp;amp;*-]+(?:\\.[a-zA-Z0-9_+&amp;amp;*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$&quot;)) {
                Toast.makeText(getApplicationContext(), &quot;이메일 형식이 잘못되었습니다. 다시 입력해주세요.&quot;, Toast.LENGTH_SHORT).show();
                return;
            }

            // &quot;기타(직접입력)&quot; 이 선택되어있으면 이메일아이디 입력란으로 이메일 값 할당, 그게 아닐 경우 콤보박스 값까지 받아서 이메일 값 할당
            if (selectedEmail.equals(&quot;기타(직접입력)&quot;)) {
                fullEmailVal = emailId;
            } else {
                fullEmailVal = emailId + &quot;@&quot; + selectedEmail;
            }

            // 서버 통신 요청
            EmailCheckRequest request = new EmailCheckRequest(fullEmailVal);

            // 이메일 중복 여부 판단
            apiService.checkEmail(request).enqueue(new Callback&amp;lt;EmailCheckResponse&amp;gt;() {
                @Override
                public void onResponse(@NonNull Call&amp;lt;EmailCheckResponse&amp;gt; call, @NonNull Response&amp;lt;EmailCheckResponse&amp;gt; response) {
                    Log.d(&quot;RegisterActivity&quot;, &quot;응답 성공: &quot; + response.body());

                    if (response.isSuccessful() &amp;amp;&amp;amp; response.body() != null) {
                        if (response.body().exists) {
                            Toast.makeText(getApplicationContext(), &quot;이미 사용 중인 이메일입니다.&quot;, Toast.LENGTH_SHORT).show();
                            isCheckEmail = false;
                        } else {
                            Toast.makeText(getApplicationContext(), &quot;사용 가능한 이메일입니다.&quot;, Toast.LENGTH_SHORT).show();
                            isCheckEmail = true;
                        }
                    }
                }

                @Override
                public void onFailure(@NonNull Call&amp;lt;EmailCheckResponse&amp;gt; call, @NonNull Throwable t) {
                    Log.e(&quot;RegisterActivity&quot;, &quot;API 요청 실패: &quot;, t);
                    Toast.makeText(getApplicationContext(), &quot;통신 오류: &quot; + t.getMessage(), Toast.LENGTH_SHORT).show();
                }
            });
        });

        // &quot;로그인 화면으로&quot; 버튼 클릭 시 로그인 페이지로 이동 (애니메이션 구현)
        loginButton.setOnClickListener(e3 -&amp;gt; {
            Intent intent = new Intent(getApplicationContext(), LoginActivity.class);
            startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());
        });

        // &quot;메인으로 이동&quot; 버튼 클릭 시 메인 페이지로 이동 (애니메이션 구현)
        mainButton.setOnClickListener(e4 -&amp;gt; {
            Intent intent = new Intent(getApplicationContext(), MainActivity.class);
            startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());
        });

    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. ApiService POST 요청 메서드 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 서버로 요청할 때의 URL 정의와 파라미터 설정을 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;URL은 백엔드에서 설정했던 URL과 무조건 동일하게 설정&lt;/b&gt;&lt;/span&gt;을 하고, 파라미터는 만들어놓은 EmailCheckRequest 파일로 해놓았습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748868682599&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@POST(&quot;/api/android/check-email&quot;)
Call&amp;lt;EmailCheckResponse&amp;gt; checkEmail(@Body EmailCheckRequest request);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java/Android</category>
      <category>spring boot</category>
      <category>모바일앱개발</category>
      <category>안드로이드</category>
      <author>gihyeoon</author>
      <guid isPermaLink="true">https://studydeveloper.tistory.com/44</guid>
      <comments>https://studydeveloper.tistory.com/44#entry44comment</comments>
      <pubDate>Mon, 2 Jun 2025 21:55:36 +0900</pubDate>
    </item>
    <item>
      <title>[Android 공부] Spring Boot와 연계를 통한 로그인 구현 (1)</title>
      <link>https://studydeveloper.tistory.com/41</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 진행 중이던 프로젝트 &quot;FlipMarket&quot;을 안드로이드 버전으로도 개발하면 괜찮겠다고 생각해서 웹 개발과 병행하며 진행 중에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 이 프로젝트는 할 필요 없었는데, 곧 선발전 대회가 있어서 대비할 겸 공부하자 생각해서 진행하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;백엔드 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 필요한 의존성 추가 및 DB 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security + JWT를 활용해서 로그인을 구현할 것이므로 이와 관련된 라이브러리 의존성을 추가했습니다. 그 외의 것들은 이 후에 개발할 때 편의를 위함과 기본적인 라이브러리들을 추가했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748437962307&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.mysql:mysql-connector-j'
	runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
	runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' // or jjwt-gson
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 연결의 경우 MySQL을 사용해서 진행할 것이고, 이에 따른 &lt;b&gt;driver 설정 및 url 설정을 application.properties&lt;/b&gt;에 하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spring.datasource.url 부분에 localhost:3306/StudyProject?... 쪽의 StudyProject는 연결할 스키마의 명칭이므로 다르게 설정하셨을 경우 그에 맞게 수정하셔야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748438035173&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring.application.name=FlipMarketAndroidBackend

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/StudyProject?useSSL=false&amp;amp;useUnicode=true&amp;amp;serverTimezone=Asia/Seoul
spring.datasource.username=root
spring.datasource.password=1234

#true 설정 시 jpa 쿼리문 확인 가능
spring.jpa.show-sql=true

#DDL 정의시 DB의 고유 기능을 사용 가능
spring.jpa.hibernate.ddl-auto=update

# JPA의 구현체인 Hibernate가 동작하면서 발생한 SQL의 가독성을 높여줌
spring.jpa.properties.hibernate.format_sql=true

# 트랜잭션이 끝날 경우 connection이 반납될 수 있도록 설정
spring.jpa.open-in-view=false&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 엔티티 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 PK값을 넣고, 이메일, 비밀번호, 이름, 나이, 전화번호 값을 가지고 있습니다. 이 외의 role 컬럼은 유저/어드민 분류를 위해 추가하였고, createdAt, updatedAt은 회원가입 및 정보 수정 이후 언제 해당 이벤트가 이루어졌는지를 저장하기 위해 추가했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748438227057&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.time.LocalDateTime;

import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = &quot;user&quot;)
public class User {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long num;
	
	@Column(nullable = false)
	private String email;
	
	@Column(nullable = false)
	private String pwd;
	
	@Column(nullable = false)
	private String name;
	
	@Column(nullable = false)
	private int age;
	
	private String phoneNum;
	
	private String role;
	
	@CreationTimestamp
	private LocalDateTime createdAt;
	
	@UpdateTimestamp
	private LocalDateTime updatedAt;
	
	public User() {
		// TODO Auto-generated constructor stub
	}
	
	public User(String email, String pwd, String name, int age, String phoneNum, String role) {
		this.email = email;
		this.pwd = pwd;
		this.name = name;
		this.age = age;
		this.phoneNum = phoneNum;
		this.role = role;
	}
	
	public Long getNum() {
		return num;
	}
	
	public String getEmail() {
		return email;
	}

	public String getPwd() {
		return pwd;
	}
	
	public String getPhoneNum() {
		return phoneNum;
	}
	
	public int getAge() {
		return age;
	}
	
	public String getName() {
		return name;
	}

	public String getRole() {
		return role;
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. User 엔티티 Repository 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인, 회원가입, 회원 조회 및 수정 등등 사용자와 관련된 DB 작업들을 담당할 Repository를 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;findByEmail&lt;/b&gt; 메서드는 Email(아이디) 값을 통해 유저 정보를 조회하는 쿼리입니다. 후에 로그인 처리 시 사용할 쿼리입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748438348922&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.NativeQuery;
import org.springframework.stereotype.Repository;

import com.lgh.FlipMarketBackend.entity.User;

@Repository
public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {
	
    @NativeQuery(&quot;SELECT * FROM user WHERE email = ?1&quot;)
	Optional&amp;lt;User&amp;gt; findByEmail(String email);
    
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. JWT 필터 및 인증 설정 파일 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Spring Security&lt;/b&gt;를 사용하게 되면 &lt;b&gt;모든 인증이 제한&lt;/b&gt;됩니다. 그렇기 때문에 이후에 api &lt;span style=&quot;color: #ee2323;&quot;&gt;url 요청할 때 접근을 실패하여 정상 작동이 안될 수도 있습니다&lt;/span&gt;. 이를 방지할 파일 하나를 추가했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;/api/android/login&quot; url을 접근 허용하였는데, 해당 url은 추후에 프론트에서 요청할 url입니다. 미리 설정해놨습니다&lt;/p&gt;
&lt;pre id=&quot;code_1748438458994&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig { // JWT 필터 및 인증 설정 담당

	@Bean
	SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http.csrf(csrf -&amp;gt; csrf.disable()).cors(cors -&amp;gt; {
		}).authorizeHttpRequests(
				auth -&amp;gt; auth.requestMatchers(&quot;/api/android/login&quot;).permitAll().anyRequest().authenticated())
				.sessionManagement(sess -&amp;gt; sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

		return http.build();
	}

	@Bean
	PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. CORS 설정 파일 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android &amp;lt;-&amp;gt; Spring Boot 간 API 요청을 자유롭게 해줄 수 있도록 CORS를 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CORS&lt;/b&gt;가 뭐냐면 다른 곳에서 현재 실행 중인 애플리케이션에 요청을 보내는 것에 대해 허용해줄 수 있게 하는 정책입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) localhost:1234 에서 localhost:8080 로 request 할 경우 CORS 설정이 되어있지 않으면 기본적으로 요청을 거부함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 허용해야하만 하는 이유는, 지금 Android 앱에서 Boot 서버로 요청을 보내는 상황인데, 그렇게 되면 CORS 정책 상 요청을 거부하게 됩니다. 그렇게 되면 로그인 시 서버 쪽에서 거부하여 계속해서 실패 값을 반환할 수도 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748438617694&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {

	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping(&quot;/**&quot;).allowedOrigins(&quot;*&quot;).allowedMethods(&quot;*&quot;);
	}
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 토큰 생성 및 검증 파일 추가&lt;/h3&gt;
&lt;pre id=&quot;code_1748439029921&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.nio.charset.StandardCharsets;
import java.util.Date;

import javax.crypto.SecretKey;

import org.springframework.stereotype.Service;

import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

@Service
public class JwtService { // 토큰 생성 및 검증을 담당

	// JWT 서명을 위한 비밀 키 *실제 서버 운영 시에는 .properties 파일에 따로 정의해야함
	private final String secretKey = &quot;VoNP8e8GvbCPMJoXeZSCaHUSvJFk2Mav&quot;;
	
	public String generatedToken(String username) {
		SecretKey key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
		
		return Jwts.builder()
				.setSubject(username)
				.setIssuedAt(new Date()) // 토큰 발급 시간 (현재 시간)
				.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) // 토큰 발급 시간은 1시간 유효로 설정
				.signWith(key, SignatureAlgorithm.HS256) // 서명 알고리즘은 SHA-256 알고리즘 사용
				.compact();
	}
	
	public String extractUsername(String token) {
		return Jwts.parserBuilder()
				.setSigningKey(secretKey.getBytes()) // SigningKey를 지정해야 위조되지 않은 토큰임을 증명할 수 있음.
				.build()
				.parseClaimsJws(token)
				.getBody()
				.getSubject();
	}
	
	// 토큰의 유효성을 확인
	public boolean validateToken(String token) {
		try {
			Jwts.parserBuilder().setSigningKey(secretKey.getBytes()).build().parseClaimsJws(token);
			return true;
		} catch (JwtException e) {
			return false;
		}
	}
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. JWT 토큰 유효 검사 및 Security 등록하는 파일 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 요청이 들어올 경우, JWT 토큰이 유효한지 검사하고, 인증 정보를 Security로 등록하는 역할을 하는 파일입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 설정이 있어야 &lt;b&gt;로그인 시 JWT 토큰을 검증하여 인증된 사용자로 처리&lt;/b&gt;합니다. =&amp;gt; 이를 안 해주면 &lt;span style=&quot;color: #ee2323;&quot;&gt;토큰이 있어도 무조건적으로 인증되지 않은 사용자로 간주하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1748439521945&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.IOException;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter { // 요청마다 한 번씩만 실행되는 필터

	private final JwtService jwtService;
	private final UserDetailsService userDetailsService;
	
	public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
		this.jwtService = jwtService;
		this.userDetailsService = userDetailsService;
	}

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		String authHeader = request.getHeader(&quot;Authorization&quot;); // Android에서 보낸 Authorization 헤더를 읽음
		String token = null;
		String username = null;

		// Authorization: Bearer &quot;토큰&quot; 형태의 헤더에서 실제 JWT 토큰 추출
		if (authHeader != null &amp;amp;&amp;amp; authHeader.startsWith(&quot;Bearer &quot;)) {
			token = authHeader.substring(7);
			if (jwtService.validateToken(token)) {
				username = jwtService.extractUsername(token);
			}
		}

		if (username != null &amp;amp;&amp;amp; SecurityContextHolder.getContext().getAuthentication() == null) {
			UserDetails userDetails = userDetailsService.loadUserByUsername(username);
			UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null,
					userDetails.getAuthorities());
			SecurityContextHolder.getContext().setAuthentication(authToken);
		}
		filterChain.doFilter(request, response);
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 로그인 요청 및 응답 DTO 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LoginRequest (요청), LoginResponse (응답) DTO를 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청에는 아이디, 비밀번호 값이 들어가고, 응답에는 토큰 값이 들어갑니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748439773515&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class LoginRequest {
	
	private String username;
	private String password;
	
	public String getUsername() {
		return username;
	}
	
	public String getPassword() {
		return password;
	}
	
	public void setUsername(String username) {
		this.username = username;
	}
	
	public void setPassword(String password) {
		this.password = password;
	}
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1748439783893&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class LoginResponse {

	private String accessToken;
	
	public LoginResponse(String accessToken) {
		this.accessToken = accessToken;
	}
	
	public String getAccessToken() {
		return accessToken;
	}
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 로그인을 처리하는 Service 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 로그인을 처리하는 로직인데, 이전에 추가했던 &lt;b&gt;findByEmail&lt;/b&gt;을 통해 &lt;span style=&quot;color: #006dd7;&quot;&gt;요청받은 이메일 값 (username)&lt;/span&gt;으로 &lt;b&gt;사용자 정보가 있는지 1차적으로 조회&lt;/b&gt;하고, &lt;span style=&quot;color: #006dd7;&quot;&gt;요청받은 비밀번호 값 (password)&lt;/span&gt;을 암호화했을 때 &lt;b&gt;조회된 사용자의 비밀번호 값과 동일한지 2차적으로 검사&lt;/b&gt;한 후 이메일 값 (username)을 토큰에 넣습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 내용이긴한데, 서비스를 구성할 때 사용자의 비밀번호의 경우 무조건 암호화를 하고서 DB에 넣어야합니다. 암호화는 보통 &lt;b&gt;BCryptPasswordEncoder&lt;/b&gt;를 사용해서 진행하는데, 이의 경우 복호화는 불가능한 알고리즘이라는 점 알고 있으시면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748439838862&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import com.lgh.FlipMarketBackend.config.JwtService;
import com.lgh.FlipMarketBackend.entity.User;
import com.lgh.FlipMarketBackend.repository.UserRepository;

@Service
public class AuthService {

	private final JwtService jwtService;
	private final UserRepository userRepository;
	private final BCryptPasswordEncoder passwordEncoder;

	public AuthService(JwtService jwtService, UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) {
		this.jwtService = jwtService;
		this.userRepository = userRepository;
		this.passwordEncoder = passwordEncoder;
	}

	public String login(String username, String password) {
		User user = userRepository.findByEmail(username).orElseThrow(() -&amp;gt; new UsernameNotFoundException(&quot;사용자 없음&quot;));
		
		if (!passwordEncoder.matches(password, user.getPwd())) {
			throw new BadCredentialsException(&quot;비밀번호 불일치&quot;);
		}
		
		return jwtService.generatedToken(username);
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 로그인 요청 URL을 처리하는 Controller 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;controller는 단순합니다. 일단 데이터를 처리하는 controller이기 때문에 @RestController로 선언해주고, 요청받은 이메일 값과 비밀번호 값을 통해 로그인을 실시합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748440163199&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.lgh.FlipMarketBackend.dto.LoginRequest;
import com.lgh.FlipMarketBackend.dto.LoginResponse;
import com.lgh.FlipMarketBackend.service.AuthService;


@RestController
@RequestMapping(&quot;/api/android/&quot;)
public class AuthController {

	private final AuthService authService;

	public AuthController(AuthService authService) {
		this.authService = authService;
	}

	@PostMapping(&quot;/login&quot;)
	public ResponseEntity&amp;lt;?&amp;gt; login(@RequestBody LoginRequest request) {
		String token = authService.login(request.getUsername(), request.getPassword());
		return ResponseEntity.ok(new LoginResponse(token));
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 길어졌는데, 이렇게 10가지 단계를 통해 백엔드 쪽은 완성되었습니다. Spring Security + JWT 구조로 인해 많이 복잡하실텐데, 보안에 강화된 로그인, 회원가입을 구현하기 위해서는 불가피한 과정이라고 생각하시면 되겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프론트 (Android) 설정&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1. 필요한 의존성 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Android에서 서버와 통신할 때 자주 사용하는 Retrofit 라이브러리와 간편한 네트워크 요청을 위한 라이브러리들입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748440388367&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Retrofit
implementation(&quot;com.squareup.retrofit2:retrofit:2.9.0&quot;)

// Gson Converter (JSON 직렬화/역직렬화용)
implementation(&quot;com.squareup.retrofit2:converter-gson:2.9.0&quot;)

// OkHttp Logging (네트워크 로그 디버깅용, 선택 사항)
implementation(&quot;com.squareup.okhttp3:logging-interceptor:4.9.3&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 로그인 요청 및 응답 파일 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 boot 쪽에서 만들었던 LoginRequest, Response 파일과 거의 동일합니다. 다만, 프론트쪽의 Request 파일은 getter가 따로 필요없습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748440452152&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.lgh.flipmarketandroid.dto;

public class LoginRequest {

    private String username;
    private String password;

    public LoginRequest(String username, String password) {
        this.username = username;
        this.password = password;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1748440457420&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.lgh.flipmarketandroid.dto;

public class LoginResponse {

    private String accessToken;

    public String getAccessToken() {
        return accessToken;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Retrofit 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot와 통신할 때 사용하는 파일입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748440514471&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import android.content.Context;
import android.content.SharedPreferences;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class RetrofitClient {

    private static final String BASE_URL = &quot;http://192.168.219.105:8080&quot;;
    private static Retrofit retrofit = null;

    public static Retrofit getInstance(final Context context) {
        OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(chain -&amp;gt; {
                    Request original = chain.request();
                    SharedPreferences prefs = context.getSharedPreferences(&quot;app_prefs&quot;, Context.MODE_PRIVATE);
                    String token = prefs.getString(&quot;jwt_token&quot;, null);

                    Request.Builder builder = original.newBuilder();
                    if (token != null) {
                        // 토큰이 존재할 경우 요청 헤더에 Authorization: Bearer &quot;토큰&quot; 형식으로 추가합니다.
                        // 이는 이전에 백엔드 쪽에서 받을 때 적었던 형식과 동일합니다.
                       builder.header(&quot;Authorization&quot;, &quot;Bearer &quot; + token);
                    }
                    Request request = builder.build();
                    return chain.proceed(request);
                })
                .build();

        if (retrofit == null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create()) // JSON 데이터를 자동으로 객체 변환
                    .build();
        }
        return retrofit;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Api 요청 파일 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 앱에서 로그인 요청 시 실제로 요청을 보낼 URL과 데이터들을 정의해주는 파일을 추가할 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;POST 어노테이션&lt;/b&gt;은 이전에 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Spring Boot에서 설정했던 PostMapping URL&lt;/b&gt;&lt;/span&gt;을 적어주시면 되고, 파라미터로는 이메일, 비밀번호를 넘길 것이므로 이전에 추가했던 LoginRequest 파일로 인자 값을 넘깁니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748440600367&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.lgh.flipmarketandroid.dto.LoginRequest;
import com.lgh.flipmarketandroid.dto.LoginResponse;

import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.POST;

public interface ApiService {

    @POST(&quot;/api/android/login&quot;)
    Call&amp;lt;LoginResponse&amp;gt; login(@Body LoginRequest request);

}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 로그인 화면 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI는 솔직히 원하시는대로 개발하시는 것이기 때문에 아래 코드는 참고만 해주시면 될 것 같습니다. ImageView는 아직 src를 추가 안했는데, &lt;s&gt;적당한 이미지를 못 찾아서..&lt;/s&gt; 추후에 추가하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748440777838&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    android:padding=&quot;24dp&quot;&amp;gt;

    &amp;lt;!-- 앱 로고 --&amp;gt;
    &amp;lt;ImageView
        android:id=&quot;@+id/logoImageView&quot;
        android:layout_width=&quot;100dp&quot;
        android:layout_height=&quot;100dp&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintBottom_toTopOf=&quot;@id/username&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;/&amp;gt;

    &amp;lt;!-- 아이디 입력 --&amp;gt;
    &amp;lt;EditText
        android:id=&quot;@+id/username&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:hint=&quot;아이디 또는 이메일&quot;
        android:inputType=&quot;textEmailAddress&quot;
        app:layout_constraintTop_toBottomOf=&quot;@id/logoImageView&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        android:layout_marginTop=&quot;32dp&quot;/&amp;gt;

    &amp;lt;!-- 비밀번호 입력 --&amp;gt;
    &amp;lt;EditText
        android:id=&quot;@+id/password&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:hint=&quot;비밀번호&quot;
        android:inputType=&quot;textPassword&quot;
        app:layout_constraintTop_toBottomOf=&quot;@id/username&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        android:layout_marginTop=&quot;16dp&quot;/&amp;gt;

    &amp;lt;!-- 로그인 버튼 --&amp;gt;
    &amp;lt;Button
        android:id=&quot;@+id/loginButton&quot;
        android:layout_width=&quot;0dp&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;로그인&quot;
        android:backgroundTint=&quot;@color/purple_500&quot;
        android:textColor=&quot;@android:color/white&quot;
        app:layout_constraintTop_toBottomOf=&quot;@id/password&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        android:layout_marginTop=&quot;24dp&quot;/&amp;gt;

    &amp;lt;!-- 회원가입 텍스트 --&amp;gt;
    &amp;lt;TextView
        android:id=&quot;@+id/signUpTextView&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:text=&quot;계정이 없으신가요? 회원가입&quot;
        android:textColor=&quot;@color/purple_700&quot;
        android:textSize=&quot;14sp&quot;
        app:layout_constraintTop_toBottomOf=&quot;@id/loginButton&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        android:layout_marginTop=&quot;16dp&quot;
        android:clickable=&quot;true&quot;
        android:focusable=&quot;true&quot;/&amp;gt;

&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 로그인 Activity 파일 추가&lt;/h3&gt;
&lt;pre id=&quot;code_1748440731092&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import com.lgh.flipmarketandroid.R;
import com.lgh.flipmarketandroid.config.ApiService;
import com.lgh.flipmarketandroid.config.RetrofitClient;
import com.lgh.flipmarketandroid.dto.LoginRequest;
import com.lgh.flipmarketandroid.dto.LoginResponse;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class LoginActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        EditText usernameEditText = findViewById(R.id.username);
        EditText passwordEditText = findViewById(R.id.password);
        Button loginButton = findViewById(R.id.loginButton);

        loginButton.setOnClickListener(e -&amp;gt; {
            String username = usernameEditText.getText().toString();
            String password = passwordEditText.getText().toString();

            if (username.isEmpty() || password.isEmpty()) {
                Toast.makeText(getApplicationContext(), &quot;아이디 또는 비밀번호를 입력해주세요.&quot;, Toast.LENGTH_SHORT).show();
            }

            LoginRequest request = new LoginRequest(username, password);
            ApiService apiService = RetrofitClient.getInstance(this).create(ApiService.class);

            apiService.login(request).enqueue(new Callback&amp;lt;LoginResponse&amp;gt;() {
                @Override
                public void onResponse(@NonNull Call&amp;lt;LoginResponse&amp;gt; call, @NonNull Response&amp;lt;LoginResponse&amp;gt; response) {
                    if (response.isSuccessful() &amp;amp;&amp;amp; response.body() != null) {
                        String token = response.body().getAccessToken();

                        // save
                        getSharedPreferences(&quot;auth&quot;, MODE_PRIVATE)
                                .edit().putString(&quot;jwt&quot;, token).apply();
                        Toast.makeText(getApplicationContext(), &quot;로그인 성공&quot;, Toast.LENGTH_SHORT).show();
                    } else {
                        Toast.makeText(getApplicationContext(), &quot;로그인 실패&quot;, Toast.LENGTH_SHORT).show();
                    }
                }

                @Override
                public void onFailure(@NonNull Call&amp;lt;LoginResponse&amp;gt; call, @NonNull Throwable t) {
                    Log.e(&quot;Retrofit&quot;, &quot;Login Failed: &quot; + t.getMessage(), t);
                    Toast.makeText(getApplicationContext(), &quot;네트워크 오류&quot;, Toast.LENGTH_SHORT).show();
                }
            });
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동작 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 먼저 실행해주고, Android를 실행해주면 이메일, 비밀번호, 로그인 버튼이 존재하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 DB에 해당하는 아이디, 비밀번호 값을 가진 사용자가 존재할 경우 &quot;로그인 성공&quot; 알림을 띄우고, 없을 경우 &quot;로그인 실패&quot; 알림을 띄우게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;네트워크 오류&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저도 처음에 그랬는데, 계속해서 네트워크 오류가 발생하여서 원인을 분석해봤습니다. (네트워크 오류라고 뜨는 이유는 Toast 메시지를 네트워크 오류로 해서 그렇습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해보니 &quot;uses-permission&quot; 설정 및 &quot;networkSecurityConfig&quot; 설정을 해야한다고 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;uses-permission 설정&lt;/h3&gt;
&lt;pre id=&quot;code_1748441277461&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;uses-permission android:name=&quot;android.permission.INTERNET&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드를 AndroidManifest.xml 파일의 manifest 태그 사이 맨 위에 추가해줍니다. 이를 &lt;span style=&quot;color: #ee2323;&quot;&gt;설정 안하면 통신 자체가 불가능&lt;/span&gt;해집니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;networkSecurityConfig 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 따로 파일을 하나 추가했습니다. 경로는 res/xml/network_security_config.xml 파일입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748441362076&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;network-security-config&amp;gt;
    &amp;lt;base-config cleartextTrafficPermitted=&quot;true&quot; /&amp;gt;
&amp;lt;/network-security-config&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 완료되면 AndroidManifest.xml 파일의 application 태그의 android:networkSecurityConfig 설정을 아래와 같이 추가해줍니다. &lt;span style=&quot;color: #ee2323;&quot;&gt;Android 9 버전 이상부터 HTTP 통신이 기본적으로 차단&lt;/span&gt;되기 때문에 이제는 필수적으로 허용해야하는 설정입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1748441400138&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;android:networkSecurityConfig=&quot;@xml/network_security_config&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java/Android</category>
      <category>android 로그인 구현</category>
      <category>spring boot</category>
      <category>모바일앱개발</category>
      <category>안드로이드</category>
      <author>gihyeoon</author>
      <guid isPermaLink="true">https://studydeveloper.tistory.com/41</guid>
      <comments>https://studydeveloper.tistory.com/41#entry41comment</comments>
      <pubDate>Wed, 28 May 2025 23:11:26 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 2941번: 크로아티아 알파벳 (JAVA)</title>
      <link>https://studydeveloper.tistory.com/40</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2941&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/2941&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;899&quot; data-origin-height=&quot;869&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc9UiQ/btsNRNsfFYJ/xCvpRK2FJ98QuwY1s3XvKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc9UiQ/btsNRNsfFYJ/xCvpRK2FJ98QuwY1s3XvKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc9UiQ/btsNRNsfFYJ/xCvpRK2FJ98QuwY1s3XvKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc9UiQ%2FbtsNRNsfFYJ%2FxCvpRK2FJ98QuwY1s3XvKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;899&quot; height=&quot;869&quot; data-origin-width=&quot;899&quot; data-origin-height=&quot;869&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제설명&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열을 입력받으면 해당 문자열에 크로아티아 알파벳을 몇 개나 포함하고 있는지 출력하는 문제이다. 크로아티아 알파벳이 아닌 일반 알파벳들은 한 글자씩 센다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, &lt;span style=&quot;color: #ee2323;&quot;&gt;주의해야할 점&lt;/span&gt;은 &lt;b&gt;&quot;dz=&quot;의 경우 &quot;z=&quot; 값을 가지고 있어&lt;/b&gt; 문자열을 탐색할 때 &lt;span style=&quot;color: #006dd7;&quot;&gt;2개가 포함&lt;/span&gt;되어있다라고 값을 반환할 수도 있기에 &quot;dz=&quot;이 포함되어있을 경우에는 따로 조건을 추가해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1746883167309&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

public class Main {
	public static void main(String[] args) throws IOException {
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
		BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));

		String word = reader.readLine();

		writer.write(String.valueOf(countCroatiaAlphabet(word)));
		writer.newLine();

		writer.flush();
		writer.close();
		reader.close();
	}

	private static int countCroatiaAlphabet(String word) {
		String[] words = word.split(&quot;&quot;);
		int result = 0;
		int cnt = 0;
		boolean[] isCheckedIdx = new boolean[word.length()];

		for (int i = 1; i &amp;lt; words.length - 1; i++) {
			char firstWord = words[i - 1].charAt(0);
			char secondWord = words[i].charAt(0);
			char thirdWord = words[i + 1].charAt(0);
			String firSecThrWord = String.valueOf(firstWord) + String.valueOf(secondWord) + String.valueOf(thirdWord);

			if (firSecThrWord.equals(&quot;dz=&quot;)) {
				isCheckedIdx[i - 1] = isCheckedIdx[i] = isCheckedIdx[i + 1] = true;
				cnt++;
				result++;
			}
		}

		for (int i = 1; i &amp;lt; words.length; i++) {
			char firstWord = words[i - 1].charAt(0);
			char secondWord = words[i].charAt(0);
			String firSecWord = String.valueOf(firstWord) + String.valueOf(secondWord);

			if (firSecWord.equals(&quot;c=&quot;) || firSecWord.equals(&quot;c-&quot;) || firSecWord.equals(&quot;d-&quot;) || firSecWord.equals(&quot;lj&quot;)
					|| firSecWord.equals(&quot;nj&quot;) || firSecWord.equals(&quot;s=&quot;) || firSecWord.equals(&quot;z=&quot;)) {
				isCheckedIdx[i - 1] = isCheckedIdx[i] = true;
				result++;
			}
		}

		result -= cnt;

		for (int i = 0; i &amp;lt; isCheckedIdx.length; i++) {
			if (!isCheckedIdx[i]) {
				result++;
			}
		}

		return result;
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코드설명&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;words 배열&lt;/b&gt;은 입력받은 문자열을 한 글자씩 담고 있는 배열이다. 해당 배열은 후에 2글자, 3글자씩 문자열을 탐색해야할 때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;result 변수&lt;/b&gt;는 크로아티아 알파벳이 몇 개 포함되고 있는지 세는 변수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;cnt 변수&lt;/b&gt;는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&quot;dz=&quot;만을 위한 변수&lt;/span&gt;인데, 만약 입력받은 &lt;span style=&quot;color: #ee2323;&quot;&gt;문자열에 &quot;dz=&quot;가 몇 개 포함되어있는지 개수&lt;/span&gt;를 세고, 후에 &lt;span style=&quot;color: #ee2323;&quot;&gt;result와 뺄셈&lt;/span&gt;을 하게 된다. 뺄셈을 하는 이유는 &lt;b&gt;&quot;dz=&quot;에 &quot;z=&quot;이 포함되어있고 2글자씩 탐색할 때 &quot;z=&quot;을 또 탐색하게 되기때문에 &quot;dz=&quot;의 &quot;z=&quot;을 또 추가로 세기 때문&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;isCheckedIdx 배열&lt;/b&gt;은 크로아티아 알파벳을 탐색하여 찾았을 때 찾은 단어들의 각 인덱스에 맞게 찾았음을 저장하는 변수이다. 좀 설명이 어려워졌는데, 예시를 들어보자면, &quot;ljes=njak&quot; 의 경우 &quot;lj&quot;, &quot;s=&quot;, &quot;nj&quot; 가 크로아티아 알파벳인데, &lt;b&gt;&quot;lj&quot;의 경우 0, 1 인덱스에 위치&lt;/b&gt;해 있고&lt;b&gt;, &quot;s=&quot;은 3, 4에 위치, &quot;nj&quot;는 5, 6에 위치&lt;/b&gt;해있다&lt;b&gt;. &lt;/b&gt;그러면 이제&lt;b&gt; isCheckedIdx[0], isCheckedIdx[1], isCheckedIdx[3], isCheckedIdx[4], isCheckedIdx[5], isCheckedIdx[6] 값은 true&lt;/b&gt;가 된다&lt;b&gt;. 나머지 값들은 false.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 왜 하냐면, 우리가 크로아티아 알파벳을 찾은 후 그 외에 나머지 알파벳들을 한 글자씩 세야하는데, &lt;span style=&quot;color: #ee2323;&quot;&gt;크로아티아 알파벳을 제외한 곳에 한 글자씩 되어있는 알파벳들이 몇 개 있는지를 확인하고 추가적으로 더해줘야하기 때문&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러니 isCheckedIdx[n] 이 &lt;b&gt;true면 해당 인덱스의 알파벳은 이미 크로아티아 알파벳 확인 완료된 알파벳&lt;/b&gt;이고,&lt;b&gt; false일 경우에는 크로아티아 알파벳이 아닌 일반적인 알파벳&lt;/b&gt;이라는 뜻을 가지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 for문의 경우 3글자씩 탐색 이후 2글자씩 탐색하였고 (2글자 먼저 탐색해도 상관 x) 이후에 &quot;dz=&quot;이 포함된 만큼 result에서 빼줬으며, isCheckedIdx[n]이 false인 개수만큼 result를 플러스하여 해결했다.&lt;/p&gt;</description>
      <category>알고리즘</category>
      <author>gihyeoon</author>
      <guid isPermaLink="true">https://studydeveloper.tistory.com/40</guid>
      <comments>https://studydeveloper.tistory.com/40#entry40comment</comments>
      <pubDate>Sat, 10 May 2025 22:36:02 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 5622번: 다이얼 (JAVA)</title>
      <link>https://studydeveloper.tistory.com/39</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/5622&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.acmicpc.net/problem/5622&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1191&quot; data-origin-height=&quot;902&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceH4EF/btsNOLHzYqW/k8BAdR3BGwVQpjwaBXtTVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceH4EF/btsNOLHzYqW/k8BAdR3BGwVQpjwaBXtTVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceH4EF/btsNOLHzYqW/k8BAdR3BGwVQpjwaBXtTVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceH4EF%2FbtsNOLHzYqW%2Fk8BAdR3BGwVQpjwaBXtTVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1191&quot; height=&quot;902&quot; data-origin-width=&quot;1191&quot; data-origin-height=&quot;902&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제설명&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 다이얼마다 알파벳 3개 + 숫자 1개로 구성되어있는데, 입력받은 알파벳 값에 맞게 몇 초가 걸리는지 합계를 구하여 출력하는 문제이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(문제 예시)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;입력: ADBP &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; &lt;b&gt;A&lt;/b&gt;는 숫자 2에 있고 &lt;b&gt;3초&lt;/b&gt;가 걸림, &lt;b&gt;D&lt;/b&gt;는 숫자 3에 있고 &lt;b&gt;4초&lt;/b&gt;가 걸림, &lt;b&gt;B&lt;/b&gt;는 숫자 2에 있고 &lt;b&gt;3초&lt;/b&gt;가 걸림, &lt;b&gt;P&lt;/b&gt;는 숫자 7에 있고 &lt;b&gt;8초&lt;/b&gt;가 걸림.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 출력은 이 걸린 시간들의 합을 출력하면 됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;출력: 18&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1746683440887&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

public class Main {
	public static void main(String[] args) throws IOException {
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
		BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));

		String[] dials = reader.readLine().split(&quot;&quot;);
		int sum = 0;

		for (int i = 0; i &amp;lt; dials.length; i++) {
			char dial = dials[i].charAt(0);
			
			if (dial &amp;gt;= 'A' &amp;amp;&amp;amp; dial &amp;lt;= 'C') {
				sum += 3;
			} else if (dial &amp;gt;= 'D' &amp;amp;&amp;amp; dial &amp;lt;= 'F') {
				sum += 4;
			} else if (dial &amp;gt;= 'G' &amp;amp;&amp;amp; dial &amp;lt;= 'I') {
				sum += 5;
			} else if (dial &amp;gt;= 'J' &amp;amp;&amp;amp; dial &amp;lt;= 'L') {
				sum += 6;
			} else if (dial &amp;gt;= 'M' &amp;amp;&amp;amp; dial &amp;lt;= 'O') {
				sum += 7;
			} else if (dial &amp;gt;= 'P' &amp;amp;&amp;amp; dial &amp;lt;= 'S') {
				sum += 8;
			} else if (dial &amp;gt;= 'T' &amp;amp;&amp;amp; dial &amp;lt;= 'V') {
				sum += 9;
			} else if (dial &amp;gt;= 'W' &amp;amp;&amp;amp; dial &amp;lt;= 'Z') {
				sum += 10;
			} else {
				sum += 2;
			}
		}

		writer.write(String.valueOf(sum));
		writer.newLine();

		writer.flush();
		writer.close();
		reader.close();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코드설명&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력받은 문자열을 하나하나의 Char값으로 변환한 후, 조건문을 활용해 해당 알파벳이 몇 초가 걸리는지를 sum 변수에 덧셈한다. 그런 다음 sum값을 출력하면 끝.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 코드가 좀 노가다 느낌이 있고, 계속 생각해보니까 더 효율적인 코드도 작성 가능할 것 같습니다.&lt;/p&gt;</description>
      <category>알고리즘</category>
      <author>gihyeoon</author>
      <guid isPermaLink="true">https://studydeveloper.tistory.com/39</guid>
      <comments>https://studydeveloper.tistory.com/39#entry39comment</comments>
      <pubDate>Thu, 8 May 2025 14:53:17 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot에서 Native Query 사용</title>
      <link>https://studydeveloper.tistory.com/38</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 내가 직접 쿼리를 사용해 구현하고 싶은 것이 생길 때 사용하는 것이 JPQL or NativeQuery입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 관점으로 보면 두 개의 개념은 비슷합니다. 원래 JPA만 사용할 경우 메서드 명명규칙에 따라서 자동으로 쿼리를 생성해 주는데, 이게 아닌 개발자가 직접 원하는 결과를 반환하는 쿼리를 작성하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 차이점이라고 하면 쿼리 작성 요령이 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) &quot;user&quot;라는 테이블에서 모든 값을 가져오고 싶을 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. JPQL의 경우&lt;/p&gt;
&lt;pre id=&quot;code_1746276987224&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(value = &quot;SELECT * FROM User WHERE id = ?1&quot;)
Optional&amp;lt;User&amp;gt; selectAll(String id);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Native Query의 경우&lt;/p&gt;
&lt;pre id=&quot;code_1746277065035&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NativeQuery(value = &quot;SELECT * FROM user WHERE id = ?1&quot;)
Optional&amp;lt;User&amp;gt; selectAll(String id);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각 선언하는 어노테이션이 다른 것도 있지만, &lt;b&gt;JPQL의 경우 JPA의 엔티티를 대상으로 쿼리를 작성해야 합니다.&lt;/b&gt; 무슨 뜻이냐면 조회하려는 테이블명 및 필드명을 우리가 만들었던 &lt;b&gt;엔티티에 정의되어 있는 명칭&lt;/b&gt;으로 해야 하는 것입니다. (컬럼명 x)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 Native Query의 경우 SQL 작성하는 것과 똑같이 작성하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파라미터 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 쿼리를 작성한다고 하면 조건 없이는 작성을 안 합니다. 최소한 PK 값을 이용해서 조회한다거나, 다양한 값들을 통해 조회하겠죠. ex) 검색할 때 상품명, 가격, 카테고리 조건을 통해 조회를 해야 할 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 위와 같이 JPQL or Native Query를 사용하게 될 경우, 파라미터 값을 프론트 -&amp;gt; 백엔드로 받아서 쿼리를 작성할 때 사용하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;숫자 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건으로 사용할 파라미터가 적을 경우에는 단순하게 숫자로 사용하는 방법이 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1746277447674&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(value = &quot;SELECT * FROM User WHERE id = ?1 and name = ?2&quot;)
Optional&amp;lt;User&amp;gt; selectAll(String id, String name);

@NativeQuery(value = &quot;SELECT * FROM user WHERE id = ?1 and name = ?2&quot;)
Optional&amp;lt;User&amp;gt; selectAll(String id, String name);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;파라미터명 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파라미터가 많아질 경우에는 위의 숫자 방식을 사용하기에는 쿼리가 많이 복잡해질 수 있습니다. 그럴 때는 파라미터명을 설정해 주고 사용하는 방법이 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1746277907462&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Query(&quot;SELECT *
FROM
    User u,
    Product p
WHERE
    u.num = p.user_num AND u.num = :userNum
        AND product_name = :productName
        AND price = :price
        AND u.name = :name&quot;)
Optional&amp;lt;User&amp;gt; selectUserProduct(@Param(&quot;userNum&quot;) Long num, @Param(&quot;name&quot;) String name, @Param(&quot;productName&quot;) String productName, @Param(&quot;price&quot;) int price);

@NativeQuery(&quot;SELECT *
FROM
    user u,
    product p
WHERE
    u.num = p.user_num AND u.num = :userNum
        AND product_name = :productName
        AND price = :price
        AND u.name = :name&quot;)
Optional&amp;lt;User&amp;gt; selectUserProduct(@Param(&quot;userNum&quot;) Long num, @Param(&quot;name&quot;) String name, @Param(&quot;productName&quot;) String productName, @Param(&quot;price&quot;) int price);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보기 좋은 코드라고 하면 당연히 파라미터명을 사용한 코드입니다. 명칭을 하나하나씩 정의해 주는 건 귀찮긴 하지만.. 숫자를 사용하는 방식은 자칫 잘못하면 순서를 잘못 적어 오류가 발생할 수 있고, 이 오류는 찾기까지 오래 걸릴 수도 있습니다.&lt;/p&gt;</description>
      <category>Java/Spring</category>
      <author>gihyeoon</author>
      <guid isPermaLink="true">https://studydeveloper.tistory.com/38</guid>
      <comments>https://studydeveloper.tistory.com/38#entry38comment</comments>
      <pubDate>Sat, 3 May 2025 22:15:52 +0900</pubDate>
    </item>
    <item>
      <title>JavaScript -&amp;gt; RestController POST 통신 오류</title>
      <link>https://studydeveloper.tistory.com/37</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 프로젝트를 하던 도중 프론트-백 분리 구조를 활용해서 코드 작성 중 이유를 알 수 없는 오류가 발생하고 있었습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 어떤 로그도 안 찍히길래 뭔 문제인가... 하루를 고민했는데 해결방법을 찾았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 오류의 원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단, 문제가 뭐였냐면 javascript 단에서 ajax로 post url 요청을 controller 쪽으로 하는 상황이었는데, 회원가입 성공을 했을 경우 &lt;b&gt;요청이 controller쪽에 닿지도 못하는 문제&lt;/b&gt;가 발생했었습니다. 근데 이게 로그도 찍히는 게 아니고.. 오류 문구도 없으니까 도대체 뭐가 문제인지를 몰랐었는데.. javascript 코드 쪽은 문제가 없어 보였습니다. (아래는 제 프로젝트의 js 코드입니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1745505626108&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$.ajax({
    type: &quot;POST&quot;,
    url: &quot;/api/register&quot;,
    data: JSON.stringify(data),
    contentType: &quot;application/json&quot;,

    success: function (result) {
        alert(&quot;회원가입이 완료되었습니다.&quot;);
        window.location.href = &quot;/login&quot;;
    },
    error: function (request, status, error) {
        alert(&quot;회원가입 오류\n code: &quot; + request.status + &quot;\n error: &quot; + error);
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 controller쪽 코드도? 문제가 될 게 없었습니다. 솔직히 어려운 로직이 아니었거든요.&lt;/p&gt;
&lt;pre id=&quot;code_1745505815812&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostMapping(&quot;/register&quot;)
public ResponseEntity&amp;lt;String&amp;gt; register(@RequestBody UserDto userDto) {
    String encodedPwd = passwordEncoder.encode(userDto.getPwd());
    userDto.setPwd(encodedPwd);
    try {
        userService.register(userDto);
        return ResponseEntity.ok(&quot;success&quot;);
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(&quot;Register Fail: &quot; + e.getMessage());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 쩔쩔 매다가 Spring Security 설정 쪽의 &lt;span style=&quot;color: #ee2323;&quot;&gt;filterChain 문제&lt;/span&gt;가 있는건가 해서 확인해봤는데, 이곳에 원인이 있었습니다. 뭐 때문인지 알 것 같으신가요?&lt;/p&gt;
&lt;pre id=&quot;code_1745505949285&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf(csrf -&amp;gt; csrf.disable())
            .authorizeHttpRequests(
                    auth -&amp;gt; auth.requestMatchers(&quot;/&quot;, &quot;/login&quot;, &quot;/register&quot;, &quot;/main&quot;, &quot;/api/overlap/**&quot;).permitAll().anyRequest().authenticated())
            .formLogin(form -&amp;gt; form.loginPage(&quot;/login&quot;).loginProcessingUrl(&quot;/login&quot;)
                    .defaultSuccessUrl(&quot;/main&quot;, true).permitAll()
                    .failureHandler((request, response, exception) -&amp;gt; {
                        System.out.println(&quot;로그인 실패 : &quot; + exception.getMessage());
                        response.sendRedirect(&quot;/login?error&quot;);
                    }))
            .logout(logout -&amp;gt; {
                logout.logoutSuccessUrl(&quot;/login&quot;);
                logout.deleteCookies(&quot;JSESSIONID&quot;, &quot;remember-me&quot;); // 로그아웃 시 쿠기 삭제
            });

    System.out.println(&quot;필터 체인 로그&quot;);

    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 오류 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 &lt;span style=&quot;color: #ee2323;&quot;&gt;Spring Security를 활용해서 로그인, 회원가입을 구현하게 될 경우에는 특정 URL의 접근 권한을 설정&lt;/span&gt;해줘야합니다. 이를 안해주면 Security쪽에서 아예 막아버려요. 이를 설정하는 메서드가 무엇이냐면 &lt;b&gt;requestMatchers&lt;/b&gt; 메서드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 지금 Security쪽 코드를 보시면 제가 회원가입 요청을 하는 URL의 접근 경로는 허용이 되어있지 않습니다. (아예 포함이 안되어있기 때문) 그렇기 때문에 이것을 추가해준다면 해결이 될 것입니다..&lt;/p&gt;
&lt;pre id=&quot;code_1745506233935&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf(csrf -&amp;gt; csrf.disable())
            .authorizeHttpRequests(
                    auth -&amp;gt; auth.requestMatchers(&quot;/&quot;, &quot;/login&quot;, &quot;/register&quot;, &quot;/main&quot;, &quot;/api/**&quot;).permitAll().anyRequest().authenticated())
            .formLogin(form -&amp;gt; form.loginPage(&quot;/login&quot;).loginProcessingUrl(&quot;/login&quot;)
                    .defaultSuccessUrl(&quot;/main&quot;, true).permitAll()
                    .failureHandler((request, response, exception) -&amp;gt; {
                        System.out.println(&quot;로그인 실패 : &quot; + exception.getMessage());
                        response.sendRedirect(&quot;/login?error&quot;);
                    }))
            .logout(logout -&amp;gt; {
                logout.logoutSuccessUrl(&quot;/login&quot;);
                logout.deleteCookies(&quot;JSESSIONID&quot;, &quot;remember-me&quot;); // 로그아웃 시 쿠기 삭제
            });

    System.out.println(&quot;필터 체인 로그&quot;);

    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어차피 상위 url은 비슷하니 &quot;/api/**&quot; 로 두 개 다 포함시켰습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보면 되게 당연한 문제였는데, Spring Security에 대한 이해도가 부족한 상태에서 개발을 하다보니 이런 문제가 발생한 것 같습니다. 어떤 라이브러리, api, 프레임워크를 사용할 때는 해당 기술을 이해하고 사용하는 것이 중요한 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;그나저나 controller쪽에 setPwd 메서드가 좀 거슬리네요.. 이번 문제 해결했으니 저런 더러운 코드나 바꿔야겠습니다 ㅎㅎ..&lt;/p&gt;</description>
      <category>Java/Spring</category>
      <author>gihyeoon</author>
      <guid isPermaLink="true">https://studydeveloper.tistory.com/37</guid>
      <comments>https://studydeveloper.tistory.com/37#entry37comment</comments>
      <pubDate>Thu, 24 Apr 2025 23:54:01 +0900</pubDate>
    </item>
  </channel>
</rss>