https://uysuiiii.tistory.com/136
test-api.yaml
https://github.com/kchabin/pbl/blob/main/src/main/resources/openapi/test-api.yaml
먼저 사용자 로그인/회원가입 구현을 먼저 개발해보기로 했다. 전에 OpenLab Kotlin backend 스터디하면서 배운 지식을 활용해보자..
졸업 프로젝트니까 그동안 배운 것들을 직접 써보면서 다시 부딪혀보고 싶다는 생각이 있었다.
대략적인 서비스 흐름이다. OpenAPI 스펙문서를 두개 작성해야하는데, 하나는 기본적인 User 기능, 채팅을 보내고 응답을 받아오는 Chat 기능 api를 정의한 문서, 다른 하나는 SpringBoot와 RAG 챗봇 사이 api를 정의한 문서이다.
사실 리액트에서 바로 RAG 모델과 상호작용할 수 있지만 굳이 SpringBoot를 사용한 이유는 두가지였다.
- 공부하려고 하는 프로젝트이니 백엔드를 제대로 만들어 보자.
- python 프레임워크가 아닌 자바 스프링이 내 주력 스택이다.
그리고 RAG를 구현하느라 너무 LLM 관련 공부에만 집중한 것 같아서 내 본래 자리로 돌아가고 싶은 마음이 컸다. AI도 공부해두면 좋은 기술이라고 생각하지만 백엔드 웹 개발자를 하고 싶으니까..
OpenAPI 문서 작성
User 로그인 및 회원가입 기능과 브라우저에서 채팅 메시지를 받아서 llm에게 전달하고 그 응답을 받아서 User에게 전달하는 Chat 기능을 먼저 정의하기로 했다. FAQ 게시판 관련해서도 나중에 스키마를 추가해야하는데, 일단 좀 더 핵심 기능인 User와 Chat 기능부터 개발해보려고 했다.
Swagger Codegen
OpenAPI 명세를 기반으로 서버 코드와 클라이언트 라이브러리를 만든다.
build.gradle.kts
plugin {
//openapi gen을 위한 플러그인 설정
id("org.openapi.generator") version "6.2.0"
}
dependencies {
implementation("org.openapitools:jackson-databind-nullable:0.2.6")
}
// openapi 문서를 Generate하기 위한 설정
task<GenerateTask>("generateApiDoc") {
generatorName.set("html2")
inputSpec.set("$projectDir/src/main/resources/openapi/test-api.yaml")
outputDir.set("$buildDir/openapi/doc/")
}
task<GenerateTask>("generateApiServer") {
generatorName.set("kotlin-spring")
inputSpec.set("$projectDir/src/main/resources/openapi/test-api.yaml")
outputDir.set("$buildDir/openapi/server-code/")
apiPackage.set("swu.pbl.ppap.openapi.generated.controller")
modelPackage.set("swu.pbl.ppap.openapi.generated.model")
configOptions.set(
mapOf(
"interfaceOnly" to "true",
)
)
additionalProperties.set(
mapOf(
"useTags" to "true",
"jakarta" to "true",
"enumPropertyNaming" to "UPPERCASE"
)
)
typeMappings.set(mapOf("DateTime" to "LocalDateTime"))
importMappings.set(mapOf("LocalDateTime" to "java.time.LocalDateTime"))
}
tasks.compileKotlin {
dependsOn("generateApiServer")
}
kotlin.sourceSets.main {
kotlin.srcDirs("src/main/kotlin", "$buildDir/openapi/server-code/src/main")
}
OpenAPI Spec의 기본 구조
# 기본 영역
openapi: 3.0.0
servers:
- url: 'http://petstore.swagger.io/v2'
info:
... 생략
# tags 영역
tags:
- name: pet
description: Everything about your Pets
... 생략
# paths 영역 (endpoint uri)
paths:
/pet:
post:
... 생략
# components 영역 (스키마 영역)
components:
requestBodies:
... 생략
schemas:
... 생략
Chat 스키마
components/schemas 아래에 스키마 구조를 정의하면 나중에 openapi codegen을 했을 때 자동적으로 dto 역할을 하는 클래스, 인터페이스들이 생성된다.
채팅 관련해서 스키마는 string 오브젝트 두개로 AskRequest와 AskResponse를 작성했다.
paths에는 api의 url 경로를 작성하면 된다. /chat에서 post 메서드로 개인정보처리방침에 대해 질문하는 메시지를 request body에 담아 전송한다. schema 아래에 $ref로 아까 정의한 객체를 참조해서 해당 구조대로 데이터가 담기게 할 수 있다.
security는 bearerAuth 설정을 했는데 이건 다음 글에 설명할 JWT 관련 설정이다.
- 처음 작성할 때는 사용자 별로 채팅 이력을 구분해서 저장할거라 토큰을 헤더에 넣어서 요청을 전달하도록 했는데, 지금 생각해보면 메시지 길이에 토큰 길이까지 더하면 네트워크 부하가 발생할 수도 있지 않을까? 하는 생각이 들었다.(대규모 서비스가 될 수 있다는 가정하에) -> 멘토님께 금요일에 질문하기
응답으로는 200, 400, 401 등 상태코드와 그에 따른 응답 스키마를 정의한다.
StatusCode 관련 스키마를 따로 작성하지 않고 그냥 만들면 나중에 codegen 결과로 AskResponse 400, AskResponse 401 이런 식으로 여러개의 data class가 생성된다.
상태코드 스키마를 따로 정의해서 코드별로 data class가 분리되지 않도록 한다.
data class StatusCode(
@Schema(example = "null", description = "")
@field:JsonProperty("200") val _200: kotlin.String? = null,
@Schema(example = "Invalid user data provided", description = "")
@field:JsonProperty("400") val _400: kotlin.String? = null,
@Schema(example = "Unauthorized access", description = "")
@field:JsonProperty("401") val _401: kotlin.String? = null,
@Schema(example = "Internal server error", description = "")
@field:JsonProperty("500") val _500: kotlin.String? = null
) {
}
내가 직접 작성한 코드가 아니라 swagger codegen을 springboot에서 설정해서 자동으로 생성되도록 한 것의 결과다.
일단 String 객체로 정의해놨기 때문에 위와 같은 data class로 생성된다. 회원가입/로그인 기능을 구현하다보니 커스텀 에러 코드를 알게되어서 일단 핵심 기능 api 테스트가 잘 마무리되면 에러코드도 수정하려고 한다.
tags로 도메인을 분리할 수 있다.
users api
- POST /api/users/signup : 회원가입
- POST /api/users/login : 로그인
- POST /api/users/logout : 로그아웃
- GET /api/users/info : 사용자 상세 조회
- PUT /api/users/info : 사용자 정보 변경
- DELETE /api/users/info : 회원탈퇴
10월 2일 수요일 기준 최신 코드는 로그인 api까지 구현했다.
User 스키마
자동 생성한 User data class이다.
회원가입 시 입력받는 정보 : loginId, username, password, confirmPassword, email, userType
주의해야할 점은 User 스키마는 회원가입용이라 나중에 로그인 시 전달할 dto 스키마도 미리 만들어주는 게 좋다. 어차피 만들게 된다. 난 LoginDto라는 이름으로 loginId, password를 받아서 로그인 인증 정보로 쓰일 스키마를 작성했다.
data class User(
@Schema(example = "null", required = true, description = "사용자 아이디")
@field:JsonProperty("loginId", required = true) val loginId: kotlin.String,
@Schema(example = "null", required = true, description = "사용자 이름")
@field:JsonProperty("username", required = true) val username: kotlin.String,
@Schema(example = "null", required = true, description = "사용자 비밀번호")
@field:JsonProperty("password", required = true) val password: kotlin.String,
@Schema(example = "null", required = true, description = "사용자 비밀번호 확인")
@field:JsonProperty("confirmPassword", required = true) val confirmPassword: kotlin.String,
@Schema(example = "null", required = true, description = "사용자 이메일")
@field:JsonProperty("email", required = true) val email: kotlin.String,
@Schema(example = "null", required = true, description = "개인 or 법인 사용자 타입")
@field:JsonProperty("userType", required = true) val userType: User.UserType,
@Schema(example = "null", description = "user 테이블 pk")
@field:JsonProperty("userId") val userId: kotlin.Long? = null,
@Schema(example = "null", description = "사용 여부")
@field:JsonProperty("isActive") val isActive: kotlin.Boolean? = true,
@Schema(example = "null", description = "탈퇴 여부")
@field:JsonProperty("isWithdrawed") val isWithdrawed: kotlin.Boolean? = false,
@field:Valid
@Schema(example = "null", description = "")
@field:JsonProperty("audit") val audit: Audit? = null
) {
/**
* 개인 or 법인 사용자 타입
* Values: PRIVATE,BUSINESS
*/
enum class UserType(val value: kotlin.String) {
@JsonProperty("PRIVATE") PRIVATE("PRIVATE"),
@JsonProperty("BUSINESS") BUSINESS("BUSINESS")
}
}
Entity 생성할 때 User 엔티티를 만들려고 했는데, 이름이 겹치는 상황이 생겼었다. 그래서 개인 블로그 개발하면서 알게된 DTO와 엔티티 간 변환 함수를 여기에도 도입했다. 하나 차이가 있다면 현재 프로젝트에서는 openapi codegen으로 data class가 자동 생성되었고, 이게 dto 역할을 하기 때문에 User를 UserEntity로 바꿔줘야 한다.
전에 블로그 개발에서는 PostDTO, CommentDTO 등으로 따로 클래스를 만들었었다.
Audit 스키마
테이블 설계 시 공통으로 들어가는 생성일, 생성자, 수정일, 수정자 등을 자동으로 입력해주는 Auditing이라는 기능이 있다. 저번에 오픈랩 스터디할 때는 api spec만 작성하고 auditing 기능 구현까지는 못해봤는데, 이번엔 회원가입, 로그인 기본적인 구현은 끝났고 user 정보 불러오고 수정하는 기능 구현하고나서 할지 아니면 지금 할지 고민중이다.
data class Audit(
@Schema(example = "null", description = "생성일")
@field:JsonProperty("createdDate") val createdDate: java.time.LocalDateTime? = null,
@Schema(example = "null", description = "생성자")
@field:JsonProperty("createdBy") val createdBy: kotlin.String? = null,
@Schema(example = "null", description = "수정일")
@field:JsonProperty("modifiedDate") val modifiedDate: java.time.LocalDateTime? = null,
@Schema(example = "null", description = "수정자")
@field:JsonProperty("modifiedBy") val modifiedBy: kotlin.String? = null
) {
}
'PPAP' 카테고리의 다른 글
PPAP 개발기(1) (1) | 2024.10.01 |
---|