Skip to content
This repository was archived by the owner on Jul 7, 2025. It is now read-only.

Commit ac6d7fd

Browse files
authored
Merge pull request #125 from ASAP-Lettering/ASAP-425
ASAP-425 공통 OAuth 핸들러(AbstractOAuthRetrieveHandler) 구현 및 Google, Kakao, Naver OAuth 핸들러 리팩토링
2 parents ae29075 + 29fe293 commit ac6d7fd

File tree

6 files changed

+242
-45
lines changed

6 files changed

+242
-45
lines changed

Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/OAuthWebClientConfig.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,13 @@ class OAuthWebClientConfig {
2525
.baseUrl("https://oauth2.googleapis.com")
2626
.defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
2727
.build()
28+
29+
@Bean
30+
@Qualifier("naverWebClient")
31+
fun naverWebClient(): WebClient =
32+
WebClient
33+
.builder()
34+
.baseUrl("https://openapi.naver.com")
35+
.defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
36+
.build()
2837
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.asap.client.oauth.platform
2+
3+
import com.asap.client.oauth.OAuthRetrieveHandler
4+
import com.asap.client.oauth.exception.OAuthException
5+
import org.springframework.web.reactive.function.client.WebClient
6+
7+
abstract class AbstractOAuthRetrieveHandler<T>(
8+
private val webClient: WebClient,
9+
) : OAuthRetrieveHandler {
10+
11+
override fun getOAuthInfo(request: OAuthRetrieveHandler.OAuthRequest): OAuthRetrieveHandler.OAuthResponse {
12+
val response = webClient
13+
.get()
14+
.uri(getApiEndpoint())
15+
.header("Authorization", "Bearer ${request.accessToken}")
16+
.retrieve()
17+
.onStatus({ it.isError }, {
18+
throw OAuthException.OAuthRetrieveFailedException(getErrorMessage())
19+
})
20+
.bodyToMono(getResponseType())
21+
.block()
22+
23+
if (response == null) {
24+
throw OAuthException.OAuthRetrieveFailedException(getErrorMessage())
25+
}
26+
27+
return mapToOAuthResponse(response)
28+
}
29+
30+
/**
31+
* Returns the API endpoint URI for the specific OAuth provider
32+
*/
33+
protected abstract fun getApiEndpoint(): String
34+
35+
/**
36+
* Returns the error message for the specific OAuth provider
37+
*/
38+
protected abstract fun getErrorMessage(): String
39+
40+
/**
41+
* Returns the response type class for the specific OAuth provider
42+
*/
43+
protected abstract fun getResponseType(): Class<T>
44+
45+
/**
46+
* Maps the provider-specific response to a common OAuthResponse
47+
*/
48+
protected abstract fun mapToOAuthResponse(response: T): OAuthRetrieveHandler.OAuthResponse
49+
}

Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/GoogleOAuthRetrieveHandler.kt

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,27 @@
11
package com.asap.client.oauth.platform
22

33
import com.asap.client.oauth.OAuthRetrieveHandler
4-
import com.asap.client.oauth.exception.OAuthException
54
import org.springframework.beans.factory.annotation.Qualifier
65
import org.springframework.stereotype.Component
76
import org.springframework.web.reactive.function.client.WebClient
87

98
@Component
109
class GoogleOAuthRetrieveHandler(
11-
@Qualifier("googleWebClient") private val googleWebClient: WebClient,
12-
) : OAuthRetrieveHandler {
13-
override fun getOAuthInfo(request: OAuthRetrieveHandler.OAuthRequest): OAuthRetrieveHandler.OAuthResponse {
14-
val googleUserInfo =
15-
googleWebClient
16-
.get()
17-
.uri("/userinfo/v2/me")
18-
.header("Authorization", "Bearer ${request.accessToken}")
19-
.retrieve()
20-
.onStatus({ it.isError }, {
21-
throw OAuthException.OAuthRetrieveFailedException("Google 사용자 정보를 가져오는데 실패했습니다.")
22-
})
23-
.bodyToMono(GoogleUserInfo::class.java)
24-
.block()
10+
@Qualifier("googleWebClient") googleWebClient: WebClient,
11+
) : AbstractOAuthRetrieveHandler<GoogleOAuthRetrieveHandler.GoogleUserInfo>(googleWebClient) {
2512

26-
if (googleUserInfo == null) {
27-
throw OAuthException.OAuthRetrieveFailedException("Google 사용자 정보를 가져오는데 실패했습니다.")
28-
}
13+
override fun getApiEndpoint(): String = "/userinfo/v2/me"
2914

15+
override fun getErrorMessage(): String = "Google 사용자 정보를 가져오는데 실패했습니다."
16+
17+
override fun getResponseType(): Class<GoogleUserInfo> = GoogleUserInfo::class.java
18+
19+
override fun mapToOAuthResponse(response: GoogleUserInfo): OAuthRetrieveHandler.OAuthResponse {
3020
return OAuthRetrieveHandler.OAuthResponse(
31-
username = googleUserInfo.name,
32-
socialId = googleUserInfo.id,
33-
email = googleUserInfo.email,
34-
profileImage = googleUserInfo.picture,
21+
username = response.name,
22+
socialId = response.id,
23+
email = response.email,
24+
profileImage = response.picture,
3525
)
3626
}
3727

Infrastructure-Module/Client/src/main/kotlin/com/asap/client/oauth/platform/KakaoOAuthRetrieveHandler.kt

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,28 @@
11
package com.asap.client.oauth.platform
22

33
import com.asap.client.oauth.OAuthRetrieveHandler
4-
import com.asap.client.oauth.exception.OAuthException
54
import com.fasterxml.jackson.annotation.JsonProperty
65
import org.springframework.beans.factory.annotation.Qualifier
76
import org.springframework.stereotype.Component
87
import org.springframework.web.reactive.function.client.WebClient
98

109
@Component
1110
class KakaoOAuthRetrieveHandler(
12-
@Qualifier("kakaoWebClient") private val kakaoWebClient: WebClient,
13-
) : OAuthRetrieveHandler {
14-
override fun getOAuthInfo(request: OAuthRetrieveHandler.OAuthRequest): OAuthRetrieveHandler.OAuthResponse {
15-
val kakaoUserInfo =
16-
kakaoWebClient
17-
.get()
18-
.uri("/v2/user/me")
19-
.header("Authorization", "Bearer ${request.accessToken}")
20-
.retrieve()
21-
.onStatus({ it.isError }, {
22-
throw OAuthException.OAuthRetrieveFailedException("Kakao 사용자 정보를 가져오는데 실패했습니다.")
23-
})
24-
.bodyToMono(KakaoUserInfo::class.java)
25-
.block()
26-
27-
if (kakaoUserInfo == null) {
28-
throw OAuthException.OAuthRetrieveFailedException("Kakao 사용자 정보를 가져오는데 실패했습니다.")
29-
}
11+
@Qualifier("kakaoWebClient") kakaoWebClient: WebClient,
12+
) : AbstractOAuthRetrieveHandler<KakaoOAuthRetrieveHandler.KakaoUserInfo>(kakaoWebClient) {
3013

14+
override fun getApiEndpoint(): String = "/v2/user/me"
15+
16+
override fun getErrorMessage(): String = "Kakao 사용자 정보를 가져오는데 실패했습니다."
17+
18+
override fun getResponseType(): Class<KakaoUserInfo> = KakaoUserInfo::class.java
19+
20+
override fun mapToOAuthResponse(response: KakaoUserInfo): OAuthRetrieveHandler.OAuthResponse {
3121
return OAuthRetrieveHandler.OAuthResponse(
32-
username = kakaoUserInfo.properties.nickname,
33-
socialId = kakaoUserInfo.id,
34-
profileImage = kakaoUserInfo.properties.profileImage,
35-
email = kakaoUserInfo.kakaoAccount.email,
22+
username = response.properties.nickname,
23+
socialId = response.id,
24+
profileImage = response.properties.profileImage,
25+
email = response.kakaoAccount.email,
3626
)
3727
}
3828

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.asap.client.oauth.platform
2+
3+
import com.asap.client.oauth.OAuthRetrieveHandler
4+
import org.springframework.beans.factory.annotation.Qualifier
5+
import org.springframework.stereotype.Component
6+
import org.springframework.web.reactive.function.client.WebClient
7+
8+
@Component
9+
class NaverOAuthRetrieveHandler(
10+
@Qualifier("naverWebClient") naverWebClient: WebClient,
11+
) : AbstractOAuthRetrieveHandler<NaverOAuthRetrieveHandler.NaverApiResponse>(naverWebClient) {
12+
13+
override fun getApiEndpoint(): String = "/v1/nid/me"
14+
15+
override fun getErrorMessage(): String = "네이버 사용자 정보를 가져오는데 실패했습니다."
16+
17+
override fun getResponseType(): Class<NaverApiResponse> = NaverApiResponse::class.java
18+
19+
override fun mapToOAuthResponse(response: NaverApiResponse): OAuthRetrieveHandler.OAuthResponse {
20+
return OAuthRetrieveHandler.OAuthResponse(
21+
username = response.response.nickname,
22+
socialId = response.response.id,
23+
email = response.response.email,
24+
profileImage = response.response.profile_image,
25+
)
26+
}
27+
28+
data class NaverApiResponse(
29+
val resultcode: String,
30+
val message: String,
31+
val response: NaverUserResponse,
32+
)
33+
34+
data class NaverUserResponse(
35+
val id: String,
36+
val nickname: String,
37+
val name: String,
38+
val email: String,
39+
val gender: String,
40+
val age: String,
41+
val birthday: String,
42+
val profile_image: String,
43+
val birthyear: String,
44+
val mobile: String,
45+
)
46+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package com.asap.client.oauth.platform
2+
3+
import com.asap.client.oauth.OAuthRetrieveHandler
4+
import com.asap.client.oauth.exception.OAuthException
5+
import io.kotest.assertions.throwables.shouldThrow
6+
import io.kotest.core.spec.style.BehaviorSpec
7+
import io.kotest.matchers.shouldBe
8+
import okhttp3.mockwebserver.MockResponse
9+
import okhttp3.mockwebserver.MockWebServer
10+
import org.springframework.http.MediaType
11+
import org.springframework.web.reactive.function.client.WebClient
12+
13+
class NaverOAuthRetrieveHandlerTest :
14+
BehaviorSpec({
15+
var mockWebServer = MockWebServer().also {
16+
it.start()
17+
}
18+
var naverWebClient: WebClient =
19+
WebClient
20+
.builder()
21+
.baseUrl(mockWebServer.url("/").toString())
22+
.defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
23+
.build()
24+
var naverOAuthRetrieveHandler = NaverOAuthRetrieveHandler(naverWebClient)
25+
26+
given("OAuth 요청이 성공적으로 처리되었을 때") {
27+
val accessToken = "test-access-token"
28+
val request = OAuthRetrieveHandler.OAuthRequest(accessToken)
29+
30+
val responseBody =
31+
"""
32+
{
33+
"resultcode": "00",
34+
"message": "success",
35+
"response": {
36+
"id": "12345",
37+
"nickname": "Test User",
38+
"name": "Test Name",
39+
"email": "test@example.com",
40+
"gender": "M",
41+
"age": "20-29",
42+
"birthday": "01-01",
43+
"profile_image": "https://example.com/profile.jpg",
44+
"birthyear": "1990",
45+
"mobile": "010-1234-5678"
46+
}
47+
}
48+
""".trimIndent()
49+
50+
mockWebServer.enqueue(
51+
MockResponse()
52+
.setResponseCode(200)
53+
.setHeader("Content-Type", "application/json")
54+
.setBody(responseBody),
55+
)
56+
57+
`when`("getOAuthInfo 메소드를 호출하면") {
58+
val response = naverOAuthRetrieveHandler.getOAuthInfo(request)
59+
60+
then("올바른 OAuthResponse를 반환해야 한다") {
61+
response.username shouldBe "Test User"
62+
response.socialId shouldBe "12345"
63+
response.email shouldBe "test@example.com"
64+
response.profileImage shouldBe "https://example.com/profile.jpg"
65+
66+
// 요청 검증
67+
val recordedRequest = mockWebServer.takeRequest()
68+
recordedRequest.path shouldBe "/v1/nid/me"
69+
recordedRequest.getHeader("Authorization") shouldBe "Bearer test-access-token"
70+
}
71+
}
72+
}
73+
74+
given("API가 오류를 반환할 때") {
75+
val accessToken = "test-access-token"
76+
val request = OAuthRetrieveHandler.OAuthRequest(accessToken)
77+
78+
mockWebServer.enqueue(
79+
MockResponse()
80+
.setResponseCode(401)
81+
.setHeader("Content-Type", "application/json")
82+
.setBody("{\"error\": \"invalid_token\"}"),
83+
)
84+
85+
`when`("getOAuthInfo 메소드를 호출하면") {
86+
then("OAuthRetrieveFailedException이 발생해야 한다") {
87+
shouldThrow<OAuthException.OAuthRetrieveFailedException> {
88+
naverOAuthRetrieveHandler.getOAuthInfo(request)
89+
}
90+
}
91+
}
92+
}
93+
94+
given("응답이 null일 때") {
95+
val accessToken = "test-access-token"
96+
val request = OAuthRetrieveHandler.OAuthRequest(accessToken)
97+
98+
mockWebServer.enqueue(
99+
MockResponse()
100+
.setResponseCode(200)
101+
.setHeader("Content-Type", "application/json")
102+
.setBody("null"),
103+
)
104+
105+
`when`("getOAuthInfo 메소드를 호출하면") {
106+
then("OAuthRetrieveFailedException이 발생해야 한다") {
107+
shouldThrow<OAuthException.OAuthRetrieveFailedException> {
108+
naverOAuthRetrieveHandler.getOAuthInfo(request)
109+
}
110+
}
111+
}
112+
}
113+
})

0 commit comments

Comments
 (0)