프로젝트 개요

이번 포스트에서는 스프링 부트와 코틀린을 이용하여 간단한 웹 애플리케이션을 구축한 과정을
소개합니다.

본 애플리케이션은 사용자 등록 및 로그인 기능을 포함하고 있으며,
데이터베이스와의 연동, 보안 설정, 그리고 API 문서화까지 다루고 있습니다.

Gradle 빌드 설정

먼저 build.gradle.kts 파일을 설정하여 프로젝트의 의존성 및 플러그인을 관리합니다.
주요 설정은 다음과 같습니다.

plugins {
    id("org.springframework.boot") version "3.3.1"
    id("io.spring.dependency-management") version "1.1.5"
    id("org.jetbrains.kotlin.jvm") version "1.9.24"
    kotlin("kapt") version "1.9.24"
    id("org.jetbrains.kotlin.plugin.spring") version "1.9.24"
}

group = "org.jc"
version = "0.0.1-SNAPSHOT"

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(19))
    }
}

repositories {
    mavenCentral()
    jcenter() // Optional, if needed
}

dependencies {
    // Kotlin dependencies
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

    // Spring Boot and related dependencies
    implementation("org.springframework.boot:spring-boot-starter")
    implementation("org.springframework.boot:spring-boot-starter-validation")
    implementation("org.springframework.boot:spring-boot-starter-web")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")

    // Database
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    runtimeOnly("com.mysql:mysql-connector-j")
    runtimeOnly("com.h2database:h2")

    // Lombok
    compileOnly("org.projectlombok:lombok")
    annotationProcessor("org.projectlombok:lombok")
    testCompileOnly("org.projectlombok:lombok")
    testAnnotationProcessor("org.projectlombok:lombok")

    // Dev Tools
    developmentOnly("org.springframework.boot:spring-boot-devtools")

    // ModelMapper
    implementation("org.modelmapper:modelmapper:3.2.0")

    // Thymeleaf and Layout Dialect
    implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
    implementation("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect")

    // JPA QueryDSL
    implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
    annotationProcessor("com.querydsl:querydsl-apt:5.0.0:jakarta")
    kapt("com.querydsl:querydsl-apt:5.0.0:jakarta")
    kapt("org.springframework.boot:spring-boot-configuration-processor")

    // Swagger
    implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0")

    // Security
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6")
    testImplementation("org.springframework.security:spring-security-test")

    // OAuth2
    implementation("org.springframework.boot:spring-boot-starter-oauth2-client")

    // JWT
    implementation("io.jsonwebtoken:jjwt-api:0.12.3")
    runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3")
    runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3")

    // Crawling Library
    implementation("org.jsoup:jsoup:1.17.2")

    // Excel Library
    implementation("org.apache.poi:poi-ooxml:5.2.5")
}

kotlin {
    jvmToolchain {
        languageVersion.set(JavaLanguageVersion.of(19))
    }
    compilerOptions {
        freeCompilerArgs.add("-Xjsr305=strict")
    }
}

tasks.named<Test>("test") {
    useJUnitPlatform()
}

이 설정 파일은 다양한 의존성과 플러그인을 포함하고 있으며,
Kotlin과 Spring Boot의 최신 버전을 사용합니다.

데이터베이스와의 연동, 보안 설정, API 문서화 등 모든 필요한 기능을 포함하고 있습니다.

주요 소스 코드

1.MemberController

사용자 가입 및 로그인 페이지를 관리하는 컨트롤러입니다.

@Controller
@RequestMapping("/member")
class MemberController(
    private val memberService: MemberService,
    private val rq: ReqData
) {

    @PreAuthorize("isAnonymous()")
    @GetMapping("/join")
    fun showJoin(): String {
        return "domain/member/member/join"
    }

    @Validated
    data class JoinForm(
        @field:NotBlank val username: String,
        @field:NotBlank val password: String
    )

    @PreAuthorize("isAnonymous()")
    @PostMapping("/join")
    fun join(@Valid joinForm: JoinForm): String {
        val joinRs = memberService.join(joinForm.username, joinForm.password, "")
        return rq.redirectOrBack(joinRs, "/member/login")
    }

    @GetMapping("/login")
    fun showLogin(): String {
        return "domain/member/member/login"
    }
}

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

3.AppConfig

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

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

결론 및 느낀점

이번 프로젝트를 통해 스프링 부트와 코틀린을 사용하여 웹 애플리케이션을 개발하는 과정에서
많은 것을 배웠습니다.

스프링 부트의 자동 설정과 코틀린의 간결한 문법 덕분에 개발 속도가 크게 향상되었습니다.

또한, JWT와 OAuth2를 이용한 인증 및 인가, Swagger를 통한 API 문서화 등 다양한 기능을
적용해 보면서 실제로 어떻게 이러한 기술들이 협력하여
전체 시스템을 구축하는지 경험할 수 있었습니다.

각 기능별로 세부적인 설정과 구현이 필요했지만,
이를 통해 프로젝트의 전반적인 이해도를 높일 수 있었고,
향후 더 복잡한 시스템을 설계하고 구현하는 데 필요한 기초를 다질 수 있었습니다.
앞으로도 이러한 기술들을 더 깊이 탐구하고,
새로운 도전 과제를 통해 더 나은 개발자가 되도록 노력하겠습니다.

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

0개의 댓글