Skip to content

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

Merged
merged 11 commits into from
Jul 12, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/backend-cd-dev.yml
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
55 changes: 55 additions & 0 deletions .github/workflows/backend-ci.yml
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
4 changes: 4 additions & 0 deletions Dockerfile
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"]
2 changes: 2 additions & 0 deletions module-core/module-client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ repositories {
dependencies {
implementation(project(":module-core:module-domain"))

implementation("org.springframework.boot:spring-boot-starter-web")

testImplementation(kotlin("test"))
}

Expand Down
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
Comment on lines +5 to +12
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거는 GithubClient에서 분리한 이유가 따로 있나용? 다른 클래스에서도 사용될 것이라고 생각해서 분리한건가요?

Copy link
Contributor Author

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 로 흡수될 수도 있을 것 같기도 해요 ㅎㅎ

) {
}
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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

400대, 500대 응답 형식의 일관성을 위해 statusCode를 따로 인자로 전달해도 좋을 것 같다는 생각이 드네용..

사실 client 요청이 단순 조회 API라 에러 핸들링을 과하는 필요없을 것 같아서 이정도도 괜춘한 것 같습니다!

Copy link
Contributor Author

@hjk0761 hjk0761 Jul 11, 2025

Choose a reason for hiding this comment

The 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) {

}
9 changes: 9 additions & 0 deletions module-core/module-client/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
security:
github:
Comment on lines +1 to +2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이후 mcp도 security 하위에 위치하는 걸까요?
mcp에 대한 설정들이 추가되면 github 이랑 같은 depth일 것 같은데, security보다 client 로 가면 어떨까요..??

받아들여지지 않아도 되는 소소한 제안입니다...ㅎㅎ

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

depth 는 같겠지만, 아무래도 깃허브 토큰 혹은 MCP 토큰 도 있고, 로그인이 들어가면 Github 에 좀 더 종속적인 설정값들이 생기게 될 것 같아서 일단은 이렇게 둘께요!
MCP 가 들어오고나서 확인해봐도 좋을 것 같아요!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 넵넵
url이 security 하위에 있길래, security라는 명명 이유가 궁금했었어용 ㅎㅎ

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)
}
}
Loading