
이번 포스트에서는 스프링 부트와 코틀린을 사용하여 RESTful API를 구축하는 방법을 설명합니다.
사용자 가입 및 로그인 기능을 포함한 API를 구현하며, 데이터베이스 연동, 보안 설정, API 문서화까지 다루겠습니다.
(이전 포스팅의 REST api버전입니다.)
@RestController
@RequestMapping("/api/members")
class MemberController(
private val memberService: MemberService
) {
@PostMapping("/join")
fun join(@RequestBody @Valid joinForm: JoinForm): ResponseEntity<RespData<Member>> {
val joinRs = memberService.join(joinForm.username, joinForm.password, "")
return ResponseEntity.ok(joinRs)
}
@GetMapping("/{username}")
fun getMember(@PathVariable username: String): ResponseEntity<Member> {
val member = memberService.findByUsername(username)
return if (member != null) {
ResponseEntity.ok(member)
} else {
ResponseEntity.notFound().build()
}
}
@Data
@Validated
data class JoinForm(
@field:NotBlank val username: String,
@field:NotBlank val password: String
)
}
* @RestController 어노테이션을 사용하여 RESTful API 컨트롤러로 설정합니다.
* @PostMapping과 @GetMapping을 사용하여 POST와 GET 요청을 처리합니다.
* @RequestBody와 @PathVariable을 통해 요청 데이터를 처리합니다.
@Service
@Transactional(readOnly = true)
class MemberService(
private val memberRepository: MemberRepository,
private val passwordEncoder: PasswordEncoder
) {
@Transactional
fun join(username: String, password: String, role: String): RespData<Member> {
val existingMember = findByUsername(username)
if (existingMember != null) {
return RespData.of("400-2", "이미 존재하는 회원입니다.")
}
val roleType = when (username) {
"system", "admin" -> M_Role.ADMIN.authority
else -> M_Role.MEMBER.authority
}
val member = Member().apply {
this.username = username
this.password = passwordEncoder.encode(password)
this.roleType = roleType
}
memberRepository.save(member)
return RespData.of(
"200",
"${member.username}님 환영합니다. 회원가입이 완료되었습니다. 로그인 후 이용해주세요.",
member
)
}
fun findByUsername(username: String): Member? {
return memberRepository.findByUsername(username)
}
fun count(): Long {
return memberRepository.count()
}
}
* @Transactional 어노테이션을 사용하여 트랜잭션 처리를 합니다.
* 비즈니스 로직을 처리하며, 회원 가입 및 조회 기능을 구현합니다.
interface MemberRepository : JpaRepository<Member, Long> {
fun findByUsername(username: String): Member?
}
@Configuration
class AppConfig {
@Value("\${custom.tempDirPath}")
lateinit var tempDirPath: String
@Value("\${custom.genFile.dirPath}")
lateinit var genFileDirPath: String
@Value("\${custom.site.name}")
lateinit var siteName: String
@Value("\${custom.site.baseUrl}")
lateinit var siteBaseUrl: String
companion object {
private var resourcesStaticDirPath: String? = null
@JvmStatic
fun getResourcesStaticDirPath(): String {
if (resourcesStaticDirPath == null) {
val resource = ClassPathResource("static/")
try {
resourcesStaticDirPath = resource.file.absolutePath
} catch (e: IOException) {
throw RuntimeException(e)
}
}
return resourcesStaticDirPath!!
}
}
}
class GlobalException(
resultCode: String,
msg: String
) : RuntimeException("$resultCode $msg") {
val rsData: RespData<Unit> = RespData.of(resultCode, msg)
}
@Configuration
@Profile("prod")
class NotProd(
private val memberService: MemberService,
private val postService: PostService
) {
private val log = LoggerFactory.getLogger(NotProd::class.java)
@Bean
@Order(3)
fun initNotProd(): ApplicationRunner {
return ApplicationRunner { args ->
val memberUser1 = memberService.findByUsername("user1")
if (memberUser1 != null) return@ApplicationRunner
work1()
}
}
@Transactional
fun work1() {
val memberUser1 = memberService.join("user1", "1234", "").data
val memberUser2 = memberService.join("user2", "1234", "").data
val memberUser3 = memberService.join("user3", "1234", "").data
val memberUser4 = memberService.join("user4", "1234", "").data
if (memberUser1 == null || memberUser2 == null || memberUser3 == null || memberUser4 == null) {
return
}
val post1 = postService.write(memberUser1, "제목 1", "내용 1", true)
val post2 = postService.write(memberUser1, "제목 2", "내용 2", true)
val post3 = postService.write(memberUser1, "제목 3", "내용 3", false)
val post4 = postService.write(memberUser1, "제목 4", "내용 4", true)
val post5 = postService.write(memberUser2, "제목 5", "내용 5", true)
val post6 = postService.write(memberUser2, "제목 6", "내용 6", false)
IntStream.rangeClosed(7, 100).forEach { i ->
postService.write(memberUser3, "제목 $i", "내용 $i", true)
postService.writeComment(memberUser1, post1, "안녕하세요! $i 댓글입니다. 잘부탁드립니다.")
postService.writeComment(memberUser2, post2, "안녕하세요! $i 댓글입니다. 잘부탁드립니다.")
postService.writeComment(memberUser3, post3, "안녕하세요! $i 댓글입니다. 잘부탁드립니다.")
postService.writeComment(memberUser4, post4, "안녕하세요! $i 댓글입니다. 잘부탁드립니다.")
postService.writeComment(memberUser1, post5, "안녕하세요! $i 댓글입니다. 잘부탁드립니다.")
}
IntStream.rangeClosed(1, 100).forEach { i ->
postService.like(memberUser2, post1)
postService.like(memberUser3, post1)
postService.like(memberUser4, post1)
postService.like(memberUser2, post2)
postService.like(memberUser3, post2)
postService.like(memberUser2, post3)
}
}
}
RESTful API를 설계하고 구현하면서, 스프링 부트와 코틀린을 사용하는 것이 얼마나 강력하고 유연한지 경험할 수 있었습니다.
특히, 코틀린의 간결한 문법과 스프링 부트의 자동 설정 기능 덕분에 개발 속도가
크게 향상되었습니다.
API 문서화 도구인 Swagger를 사용하여 API의 인터페이스를 명확히 하고, JWT와 OAuth2를 통해 보안을 강화한 점도 인상적이었습니다.
이 프로젝트를 통해 RESTful API 설계의 중요성과 스프링 부트와 코틀린을
활용한 실제 개발 경험을 쌓을 수 있었으며,
향후 더 복잡한 시스템을 설계하고 구현하는 데 도움이 될 것입니다.
계속해서 새로운 기술과 도구를 탐구하며 더 나은 개발자가 되기 위한 노력을 이어가겠습니다.