-
Notifications
You must be signed in to change notification settings - Fork 0
Client 모듈 구현 #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Client 모듈 구현 #25
Changes from 10 commits
4793a10
d6044d1
93f4b46
35de697
8c8456d
67cc4ea
e4598dc
6187f14
975791a
d5103f4
ab59b4a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
name: backend CD dev | ||
|
||
on: | ||
push: | ||
branches: | ||
- develop | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- name: Repository checkout | ||
uses: actions/checkout@v4 | ||
|
||
- name: Setup java 21 | ||
uses: actions/setup-java@v4 | ||
with: | ||
distribution: 'temurin' | ||
java-version: '21' | ||
|
||
- name: Cache gradle | ||
uses: actions/cache@v3 | ||
with: | ||
path: | | ||
~/.gradle/caches | ||
~/.gradle/wrapper | ||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} | ||
restore-keys: | | ||
${{ runner.os }}-gradle- | ||
|
||
- name: Grant permission to gradlew | ||
run: chmod +x gradlew | ||
|
||
- name: Build and test with gradle | ||
run: ./gradlew clean build | ||
|
||
- name: Sign in Dockerhub | ||
uses: docker/login-action@v1 | ||
with: | ||
username: ${{secrets.DOCKER_USERNAME}} | ||
password: ${{secrets.DOCKER_PASSWORD}} | ||
|
||
- name: Build the Docker image | ||
run: docker build -f ./Dockerfile --platform linux/arm64 --no-cache -t ossori/ossori:dev . | ||
|
||
- name: Push the Docker Image to Dockerhub | ||
run: docker push ossori/ossori:dev | ||
|
||
deploy: | ||
needs: build | ||
runs-on: [ self-hosted ] | ||
|
||
steps: | ||
- name: Pull docker image | ||
run: sudo docker pull ossori/ossori:dev | ||
|
||
- name: Docker compose up | ||
run: sudo docker compose -f ~/docker/ossori-dev-compose.yml up -d |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
name: backend CI | ||
|
||
on: | ||
pull_request: | ||
branches: | ||
- 'develop' | ||
- 'main' | ||
types: | ||
- opened | ||
- synchronize | ||
- reopened | ||
|
||
permissions: write-all | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- name: Repository checkout | ||
uses: actions/checkout@v4 | ||
|
||
- name: Setup java 21 | ||
uses: actions/setup-java@v4 | ||
with: | ||
distribution: 'temurin' | ||
java-version: '21' | ||
|
||
- name: Cache gradle | ||
uses: actions/cache@v3 | ||
with: | ||
path: | | ||
~/.gradle/caches | ||
~/.gradle/wrapper | ||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} | ||
restore-keys: | | ||
${{ runner.os }}-gradle- | ||
|
||
- name: Grant permission to gradlew | ||
run: chmod +x gradlew | ||
|
||
- name: Build and test with gradle | ||
run: ./gradlew build --info | ||
|
||
- name: Publish test results | ||
uses: EnricoMi/publish-unit-test-result-action@v2 | ||
if: always() | ||
with: | ||
files: ${{ github.workspace }}/build/test-results/test/TEST-*.xml | ||
|
||
- name: Publish test report | ||
uses: mikepenz/action-junit-report@v4 | ||
if: always() | ||
with: | ||
report_paths: ${{ github.workspace }}/build/test-results/test/TEST-*.xml |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
FROM eclipse-temurin:21-jdk | ||
ARG JAR_FILE=build/libs/*.jar | ||
COPY ${JAR_FILE} app.jar | ||
ENTRYPOINT ["java", "-jar", "-Duser.timezone=Asia/Seoul", "-Dfile.encoding=UTF-8", "/app.jar"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package client.github | ||
|
||
import dto.response.GithubRepositoriesResponse | ||
import dto.response.GithubRepositoryResponse | ||
import org.springframework.boot.context.properties.EnableConfigurationProperties | ||
import org.springframework.http.MediaType.APPLICATION_JSON | ||
import org.springframework.stereotype.Component | ||
import org.springframework.web.client.RestClient | ||
|
||
@EnableConfigurationProperties(GithubClientProperties::class) | ||
@Component | ||
class GithubClient( | ||
private val githubRestClient: RestClient, | ||
private val githubClientProperties: GithubClientProperties | ||
) { | ||
|
||
companion object { | ||
private const val AUTHORIZATION_HEADER = "Authorization" | ||
private const val AUTHORIZATION_METHOD = "Bearer" | ||
} | ||
|
||
fun getOpenSourceRepository(filterQuery: String): GithubRepositoriesResponse { | ||
val uri = "${githubClientProperties.searchRepositoryBaseUrl}$filterQuery" | ||
return defaultGithubGetClient(uri)//?q=good-first-issues:>1+help-wanted-issues:>1 | ||
} | ||
|
||
fun getRepositoryInfo(repositoryOwner: String, repositoryName: String): GithubRepositoryResponse { | ||
val uri = "${githubClientProperties.repositoryBaseUrl}/$repositoryOwner/$repositoryName" | ||
return defaultGithubGetClient(uri) | ||
} | ||
|
||
private fun getGithubToken(): String { | ||
return "$AUTHORIZATION_METHOD ${githubClientProperties.token}" | ||
} | ||
|
||
private inline fun <reified T> defaultGithubGetClient(uri: String): T { | ||
val response = githubRestClient.get() | ||
.uri(uri) | ||
.accept(APPLICATION_JSON) | ||
//.header(AUTHORIZATION_HEADER, getGithubToken()) | ||
.retrieve() | ||
.body(T::class.java) | ||
return response ?: throw RuntimeException("Response body is null") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package client.github | ||
|
||
import org.springframework.boot.context.properties.ConfigurationProperties | ||
|
||
@ConfigurationProperties(prefix = "security.github") | ||
data class GithubClientProperties( | ||
|
||
val repositoryBaseUrl: String, | ||
|
||
val searchRepositoryBaseUrl: String, | ||
|
||
val token: String | ||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package client.github | ||
|
||
import global.exception.GithubResponseException | ||
import org.springframework.http.HttpMethod | ||
import org.springframework.http.client.ClientHttpResponse | ||
import org.springframework.web.client.ResponseErrorHandler | ||
import java.net.URI | ||
|
||
class GithubResponseErrorHandler : ResponseErrorHandler { | ||
override fun hasError(response: ClientHttpResponse): Boolean { | ||
return response.statusCode.is4xxClientError || response.statusCode.is5xxServerError | ||
} | ||
|
||
override fun handleError(url: URI, method: HttpMethod, response: ClientHttpResponse) { | ||
if (response.statusCode.is4xxClientError) { | ||
throw GithubResponseException("Status code: ${response.statusCode}, Description: ${response.body}") | ||
} | ||
if (response.statusCode.is5xxServerError) { | ||
throw GithubResponseException("Github Server Unavailable") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 400대, 500대 응답 형식의 일관성을 위해 statusCode를 따로 인자로 전달해도 좋을 것 같다는 생각이 드네용.. 사실 client 요청이 단순 조회 API라 에러 핸들링을 과하는 필요없을 것 같아서 이정도도 괜춘한 것 같습니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 혹여나 400대 에러 반환에 대해서 별다른 조치를 취할 수도 있겠다는 생각에 일단 메시지를 분리했었는데요, 응답 형식이 일관될 때 프론트에서든 처리가 더 용이하겠다는 생각이 들어 메시지를 통일하도록 수정할께요! 좋은 의견 고마워요 ㅎㅎ 추가적으로 GithubResponseException 도 기존 메시지 구조를 가져가도록 수정했어요. 예외를 던지는 입장에선 상태코드와 메시지만을 전달하도록 해봤어요. |
||
} | ||
throw GithubResponseException(response.body.toString()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package config | ||
|
||
import client.github.GithubResponseErrorHandler | ||
import org.springframework.context.annotation.Bean | ||
import org.springframework.context.annotation.Configuration | ||
import org.springframework.http.client.ClientHttpRequestFactory | ||
import org.springframework.http.client.SimpleClientHttpRequestFactory | ||
import org.springframework.web.client.RestClient | ||
import java.time.Duration | ||
|
||
@Configuration | ||
class RestClientConfig { | ||
|
||
@Bean | ||
fun githubRestClient(): RestClient { | ||
val requestFactory = simpleRequestFactory() | ||
return RestClient.builder() | ||
.requestFactory(requestFactory) | ||
.defaultStatusHandler(GithubResponseErrorHandler()) | ||
.build() | ||
} | ||
|
||
private fun simpleRequestFactory(): ClientHttpRequestFactory { | ||
val requestFactory = SimpleClientHttpRequestFactory() | ||
requestFactory.setConnectTimeout(Duration.ofSeconds(2)) | ||
requestFactory.setReadTimeout(Duration.ofSeconds(10)) | ||
|
||
return requestFactory | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package dto.response | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty | ||
|
||
data class GithubRepositoriesResponse( | ||
|
||
@JsonProperty("total_count") | ||
val totalCount: Long, | ||
|
||
val items: List<GithubRepositoryResponse> | ||
|
||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package dto.response | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty | ||
|
||
data class GithubRepositoryResponse( | ||
|
||
val id: Long, | ||
|
||
val name: String, | ||
|
||
val description: String?, | ||
|
||
@JsonProperty("html_url") | ||
val githubLink: String, | ||
|
||
@JsonProperty("stargazers_count") | ||
val countingStar: Long, | ||
|
||
@JsonProperty("open_issues_count") | ||
val issueCount: Long, | ||
|
||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package global.exception | ||
|
||
class GithubResponseException(message: String) : RuntimeException(message) { | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
security: | ||
github: | ||
Comment on lines
+1
to
+2
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이후 mcp도 security 하위에 위치하는 걸까요? 받아들여지지 않아도 되는 소소한 제안입니다...ㅎㅎ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. depth 는 같겠지만, 아무래도 깃허브 토큰 혹은 MCP 토큰 도 있고, 로그인이 들어가면 Github 에 좀 더 종속적인 설정값들이 생기게 될 것 같아서 일단은 이렇게 둘께요! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아하 넵넵 mcp에 어떤 값들이 필요하거나 들어올지 모르니 일단 그대로 가시죠! |
||
repository_base_url: https://api.github.com/repos | ||
search_repository_base_url: https://api.github.com/search/repositories | ||
token: test_pat | ||
|
||
spring: | ||
jpa: | ||
defer-datasource-initialization: true |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package client.github | ||
|
||
import org.junit.jupiter.api.Assertions.assertNotNull | ||
import org.junit.jupiter.api.Disabled | ||
import org.junit.jupiter.api.extension.ExtendWith | ||
import org.springframework.beans.factory.annotation.Autowired | ||
import org.springframework.boot.test.context.TestConfiguration | ||
import org.springframework.context.annotation.ComponentScan | ||
import org.springframework.test.context.ContextConfiguration | ||
import org.springframework.test.context.TestPropertySource | ||
import org.springframework.test.context.junit.jupiter.SpringExtension | ||
import kotlin.test.Test | ||
|
||
@TestConfiguration | ||
@ComponentScan(basePackages = ["client", "config"]) | ||
class TestConfig() { | ||
} | ||
|
||
@TestPropertySource( | ||
properties = [ | ||
"security.github.repository_base_url=https://api.github.com/repos/", | ||
"security.github.search_repository_base_url=https://api.github.com/search/repositories", | ||
"security.github.token=dummy-token" | ||
] | ||
) | ||
@ExtendWith(SpringExtension::class) | ||
@ContextConfiguration(classes = [TestConfig::class]) | ||
class GithubClientTest( | ||
|
||
@Autowired | ||
val githubClient: GithubClient | ||
) { | ||
|
||
@Disabled | ||
@Test | ||
fun repositoryTest() { | ||
val repositoryInfo = githubClient.getRepositoryInfo("Almumol", "OSSori") | ||
assertNotNull(repositoryInfo) | ||
} | ||
|
||
@Disabled | ||
@Test | ||
fun searchRepositoryTest() { | ||
var openSourceRepository = githubClient.getOpenSourceRepository("?q=good-first-issues:>0") | ||
assertNotNull(openSourceRepository) | ||
openSourceRepository = githubClient.getOpenSourceRepository("?q=help-wanted-issues:>0") | ||
assertNotNull(openSourceRepository) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이거는 GithubClient에서 분리한 이유가 따로 있나용? 다른 클래스에서도 사용될 것이라고 생각해서 분리한건가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이렇게 구현한 의도는 앞에서 몰리가 질문해준 것과 비슷해요 참고. security 라는 분류 아래에 여러 외부 API 이용을 위한 값들을 위치시키려는 의도였는데, 지금은 구현된 부분인 Github Rest API 를 위한 값들만 들어가 있어요.
하지만 나중엔 예를들어 Github OAuth 와 관련된 값 등 Github 와는 연관있지만 Rest API 와는 다른 부분의 값들이 들어올 수도 있겠다고 생각했어요.
이런 생각에서 일단 GithubClientProperties 를 분리했는데, 아무래도 나중에 기능이 확장된다면 그에 맞게 설정 파일의 구조가 바뀌거나, 지금 GithubClientProperties 가 변하거나 그런 조치가 취해질 것 같아요!
알파카가 말해준 것처럼 다시 GithubClient 로 흡수될 수도 있을 것 같기도 해요 ㅎㅎ