프로젝트 개요

이번 포스트에서는 스프링 부트와 코틀린을 사용하여 RESTful API를 구축하는 방법을 설명합니다.
사용자 가입 및 로그인 기능을 포함한 API를 구현하며, 데이터베이스 연동, 보안 설정, API 문서화까지 다루겠습니다.
(이전 포스팅의 REST api버전입니다.)

Gradle 빌드 설정

build.gradle.kts 파일 설정은 앞서 소개한 내용과 동일하며, 필요한 의존성과 플러그인을 포함하고 있습니다.

주요 소스 코드

1.MemberController

사용자 관련 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을 통해 요청 데이터를 처리합니다.

2.MemberService

회원 가입 및 조회 기능을 제공하는 서비스입니다.

@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 어노테이션을 사용하여 트랜잭션 처리를 합니다.
* 비즈니스 로직을 처리하며, 회원 가입 및 조회 기능을 구현합니다.

3.MemberRepository

JPA를 이용한 회원 엔티티의 데이터베이스 CRUD를 처리하는 인터페이스입니다.

interface MemberRepository : JpaRepository<Member, Long> {
    fun findByUsername(username: String): Member?
}

4.Application Configuration

애플리케이션 설정을 관리하는 클래스입니다.

@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!!
        }
    }
}

5.GlobalException

전역 예외 처리 클래스입니다.

class GlobalException(
    resultCode: String,
    msg: String
) : RuntimeException("$resultCode $msg") {

    val rsData: RespData<Unit> = RespData.of(resultCode, msg)
}

6.Initialization Configuration

애플리케이션이 시작될 때 초기 데이터 설정을 위한 클래스입니다.

@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 설계의 중요성과 스프링 부트와 코틀린을
활용한 실제 개발 경험을 쌓을 수 있었으며,
향후 더 복잡한 시스템을 설계하고 구현하는 데 도움이 될 것입니다.

계속해서 새로운 기술과 도구를 탐구하며 더 나은 개발자가 되기 위한 노력을 이어가겠습니다.

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글