코틀린_스몰프로젝트_2

Kingyj·2025년 2월 20일

개요

  • 코틀린 공부를 위해 기본 강의도 중요하지만, 배운 것을 활용하여 회원가입과 가벼운 게시판 기능을 구현해서 서비스를 만들어보려 한다.

서비스

  • 회원 기능
  • 게시판 기능
  • 댓글 기능

정리 내용

  • 회원가입 구현
  • 로그인 로그아웃 구현
  • 앞으로 진행 계획

회원가입 구현

회원가입 Controller 코드.
이 컨트롤러는 회원가입을 처리하는 API이다.
요청이 들어오면 UserService에서 회원가입 로직을 실행하고, 결과를 UserResponse로 반환한다.

클라이언트가 POST /api/users/register 로 요청을 보낸다.
@RequestBody가 JSON 데이터를 UserRequest 객체로 변환.
userService.registerUser(request)가 회원가입 로직을 수행.
회원가입 성공 시 UserResponse 객체를 ResponseEntity.ok()로 반환.

package kotlinProject.controller

import kotlinProject.dto.UserRequest
import kotlinProject.dto.UserResponse
import kotlinProject.service.UserService
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/users")
class UserController(
    private val userService: UserService
) {
    @PostMapping("/register")
    fun registerUser(@RequestBody request: UserRequest): ResponseEntity<UserResponse> {
        return ResponseEntity.ok(userService.registerUser(request))
    }
}

User Entity 코드이다.
아래 코드에서 role: Role 은 사용자 역할이다.
@Enumerated(EnumType.STRING)Role이 ENUM 타입이며, 문자열(String) 형태로 저장됨.
@Column(nullable = false) → 필수 입력.
val role: Role = Role.USER → 기본값은 Role.USER (일반 사용자).

고로 Role Enum 클래스가 필요하다.

package kotlinProject.entity

import jakarta.persistence.*

@Entity
// 데이터베이스의 "users" 테이블과 매핑
@Table(name = "users")
data class User(
    // 기본 키 (Primary Key) 설정
    @Id
    // 자동 증가 (IDENTITY 전략 사용 - 데이터베이스가 자동으로 증가시킴)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null, // 기본값은 null (DB에서 자동 생성)

    // username 컬럼 - null 불가능, 중복 불가능 (unique)
    @Column(nullable = false, unique = true)
    val username: String,

    // password 컬럼 - null 불가능
    @Column(nullable = false)
    val password: String,

    // ENUM 타입 매핑 (DB에 문자열 형태로 저장)
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    val role: Role = Role.USER // 기본값은 USER로 설정
)

Role 이라는 Enum 클라스를 만들어서 일반사용자와 관리자를 구분했다.

package kotlinProject.entity

enum class Role {
    USER, // 일반 사용자
    ADMIN // 관리자
}

아래 코드는 회원가입 Service 코드이다.
이 코드로 중복된 사용자의 이름을 확인하고,
중복되지 않으면 비밀번호를 암호화하고 사용자를 DB에 저장한다.
저장된 사용자 정보를 응답으로 반환한다.

package kotlinProject.service

import kotlinProject.dto.UserRequest
import kotlinProject.dto.UserResponse
import kotlinProject.entity.User
import kotlinProject.repository.UserRepository
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.stereotype.Service

@Service
class UserService(
    private val userRepository: UserRepository,
    private val passwordEncoder: BCryptPasswordEncoder
) {
    fun registerUser(request: UserRequest): UserResponse {
        // 중복 체크
        if (userRepository.findByUsername(request.username).isPresent) {
            throw IllegalArgumentException("이미 존재하는 사용자입니다.")
        }

        // 비밀번호 암호화 후 저장
        val encodedPassword = passwordEncoder.encode(request.password)
        val user = userRepository.save(User(username = request.username, password = encodedPassword))

        return UserResponse(user.id, user.username) // 🔹 user.id가 nullable이므로 그대로 전달 가능
    }
}

로그인과 로그아웃 구현

JWT (JSON Web Token)를 생성하고 검증하는 도구를 제공하는 코드이다.
로그인 인증에 해당된다.

secretKey: JWT를 생성할 때 사용할 비밀 키입니다. 이 키를 사용하여 JWT를 서명하고, 누군가 이 키를 알지 않으면 토큰을 변조할 수 없습니다.
expiration: JWT 토큰의 만료 시간입니다. 여기서는 1시간 후로 설정되어 있습니다. (1시간 = 1000밀리초 × 60초 × 60분)
generateToken 함수는 JWT 토큰을 생성하는 함수입니다.
setSubject(username): JWT에 사용자 이름을 담습니다. 이를 통해 나중에 이 토큰이 누구에 의해 생성되었는지 알 수 있습니다.
claim("role", role.name): JWT에 사용자 역할(role)을 추가합니다. 예를 들어, "ADMIN"이나 "USER" 등의 역할을 담을 수 있습니다.
validateToken은 토큰이 유효한지 검사합니다.

package kotlinProject.login

import io.jsonwebtoken.*
import io.jsonwebtoken.security.Keys
import org.springframework.stereotype.Component
import java.util.*
import javax.crypto.SecretKey
import kotlinProject.entity.Role

@Component
class JwtUtil {
    private val secretKey: SecretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256) // 비밀 키 생성
    private val expiration: Long = 1000 * 60 * 60 // 1시간

    // 🔹 JWT 토큰 생성 (Role 정보 포함)
    fun generateToken(username: String, role: Role): String {
        return Jwts.builder()
            .setSubject(username) // subject 사용법 수정
            .claim("role", role.name) // 역할 추가
            .setIssuedAt(Date())
            .setExpiration(Date(System.currentTimeMillis() + expiration))
            .signWith(secretKey, SignatureAlgorithm.HS256) // 🔹 SIG 대신 signWith 사용
            .compact()
    }

    // 🔹 JWT 토큰 검증
    fun validateToken(token: String): Boolean {
        return try {
            Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)
            true
        } catch (e: Exception) {
            false
        }
    }

    // 🔹 토큰에서 사용자 이름 추출
    fun extractUsername(token: String): String? {
        return try {
            Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)
                .body.subject // subject 사용법 수정
        } catch (e: Exception) {
            null
        }
    }

    // 🔹 토큰에서 역할(Role) 추출
    fun extractRole(token: String): String? {
        return try {
            Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)
                .body["role"] as String
        } catch (e: Exception) {
            null
        }
    }
}

다음 코드는 JWT필터를 구현한 코드다. HTTP 요청에 포함된 JWT토큰을 확인하고, 그 토큰이 유요한지 검증한 후, 사용자 인증 정보를 설정하는 역할을 한다.

클라이언트가 요청을 보낼 때, Authorization 헤더에 JWT 토큰을 포함시킨다.
JwtFilter가 이 요청을 가로채어 JWT 토큰을 추출한다.
추출한 토큰을 검증하고, 토큰이 유효하다면 사용자 이름을 추출한다.
SecurityContext에 인증 정보를 설정하여, 현재 요청을 보낸 사용자가 누구인지 확인한다.
이후 필터 체인의 다음 필터나 서블릿으로 요청을 전달한다.

package kotlinProject.login

import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.filter.OncePerRequestFilter

class JwtFilter(
    private val jwtUtil: JwtUtil
) : OncePerRequestFilter() {

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val token = request.getHeader("Authorization")?.replace("Bearer ", "")

        if (token != null && jwtUtil.validateToken(token)) {
            val username = jwtUtil.extractUsername(token)

            // SecurityContext에 사용자 인증 정보 설정
            val authentication = username?.let { JwtAuthenticationToken(it) }
            SecurityContextHolder.getContext().authentication = authentication
        }

        filterChain.doFilter(request, response)
    }
}

다음 코드는 JWT 인증 토큰을 정의하는 코드이다. 사용자가 인증되었음을 나타내는 역할을 한다.

JwtAuthenticationToken은 JWT 토큰을 통해 인증된 사용자 정보를 저장한다.
이 클래스는 스프링 시큐리티에서 인증된 사용자의 정보를 나타내는 토큰 역할을 하며, 사용자 이름을 가지고 있다.
getCredentials()는 인증에 필요한 자격증명이 없으므로 null을 반환하고, getPrincipal()은 사용자의 이름을 반환하여 인증된 사용자 정보를 제공한다.
마지막으로, isAuthenticated = true로 이 토큰이 인증된 상태임을 나타낸다.

package kotlinProject.login

import org.springframework.security.authentication.AbstractAuthenticationToken

class JwtAuthenticationToken(
    private val username: String
) : AbstractAuthenticationToken(null) {

    override fun getCredentials(): Any? = null

    override fun getPrincipal(): Any = username

    init {
        isAuthenticated = true // 인증이 완료되었음을 표시
    }
}

앞으로 진행 계획

이제 테스트 코드를 작성하고, 정상 작동이 되는지 실행도 해보고 프론트 작업을 최소한이지만 진행해볼것이다.


0개의 댓글