코틀린_스몰프로젝트_3

Kingyj·2025년 2월 25일

개요

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

서비스

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

정리 내용

  • 프로젝트 중간정리
  • 테스트 코드
  • 앞으로 진행 계획

프로젝트 중간정리

LANGUAGE

  • Kotlin

FrameWork

  • Springboot

SERVER

  • Docker

DB

  • MySQL


1. 프로젝트 세팅

  • Spring Initializr 설정 (Kotlin + Spring Boot)
  • Gradle.kts 사용하여 의존성 추가
  • MySQL 연동

2. 게시판 기능 (CRUD) 구현

  • Post 엔티티, PostRepository 생성
  • PostService 작성
  • PostController에서 CRUD API 구현
  • DTO 적용하여 데이터 흐름 개선
  • Long? 관련 Type mismatch 오류 해결

3. 회원가입 & 로그인 기능 추가

  • User 엔티티 생성 id, username, password, role
  • UserRepository, UserService 작성
  • AuthController에서 회원가입 API 구현
  • 비밀번호 BCrypt 해싱 적용

4. JWT 기반 인증 & 보안 설정

  • JwtUtil.kt 작성 (토큰 생성, 검증, 사용자 정보 추출)
  • JwtFilter.kt 작성하여 SecurityContext에 인증 정보 저장
  • SecurityConfig.kt에서 Spring Security + JWT 설정
  • JwtAuthentication 관련 오류 해결

5. 관리자 계정 생성 기능 추가

  • AdminController에서 관리자 생성 API 구현
  • Use 엔티티에 Role 필드 추가 USER, ADMIN
  • 중복된 AdminController 제거하여 충돌 해결

6. 보안(권한 관리) 추가

  • SecurityConfig.kt에서
    - /api/admin/** → ADMIN 권한 필요
    - /api/posts/** → 로그인된 사용자만 접근 가능
    - /api/auth/** → 모든 사용자 접근 가능

테스트 코드

1. KotlinProjectApplicationTests

package backend

import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class KotlinProjectApplicationTests {

	@Test
	fun contextLoads() {
	}

}

2. PostServiceTest

package backend

import backend.entity.Post
import backend.repository.PostRepository
import backend.service.PostService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.MockitoAnnotations
import java.util.*

class PostServiceTest {

    @Mock
    private lateinit var postRepository: PostRepository

    @InjectMocks
    private lateinit var postService: PostService

    @BeforeEach
    fun setup() {
        MockitoAnnotations.openMocks(this)
    }

    @Test
    fun `게시글 생성 테스트`() {
        // Given
        val title = "테스트 제목"
        val content = "테스트 내용"
        val author = "테스트 작성자"
        val post = Post(id = 1L, title = title, content = content, author = author)

        `when`(postRepository.save(any(Post::class.java))).thenReturn(post)

        // When
        val savedPost = postService.createPost(title, content, author)

        // Then
        assertNotNull(savedPost)
        assertEquals(title, savedPost.title)
        assertEquals(content, savedPost.content)
        assertEquals(author, savedPost.author)
    }

    @Test
    fun `게시글 단건 조회 테스트`() {
        // Given
        val post = Post(id = 1L, title = "테스트", content = "테스트 내용", author = "작성자")
        `when`(postRepository.findById(1L)).thenReturn(Optional.of(post))

        // When
        val foundPost = postService.getPostById(1L)

        // Then
        assertNotNull(foundPost)
        assertEquals("테스트", foundPost.title)
    }

    @Test
    fun `게시글 삭제 테스트`() {
        // Given
        val post = Post(id = 1L, title = "테스트", content = "테스트 내용", author = "작성자")
        `when`(postRepository.findById(1L)).thenReturn(Optional.of(post))
        doNothing().`when`(postRepository).deleteById(1L)  // deleteById()를 모킹

        // When
        postService.deletePost(1L)

        // Then
        verify(postRepository, times(1)).deleteById(1L)  // deleteById()가 호출되었는지 검증
    }
}

3. UserControllerTest

package backend.controller

import backend.dto.UserRequest
import backend.dto.UserResponse
import backend.service.UserService
import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.any
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.test.web.servlet.setup.MockMvcBuilders


@ExtendWith(MockitoExtension::class) // 최신 Mockito 확장 사용
class UserControllerTest {

    private lateinit var mockMvc: MockMvc

    @Mock
    private lateinit var userService: UserService

    @InjectMocks
    private lateinit var userController: UserController

    private val objectMapper = ObjectMapper()

    private lateinit var userRequest: UserRequest
    private lateinit var userResponse: UserResponse

    @BeforeEach
    fun setup() {
        mockMvc = MockMvcBuilders.standaloneSetup(userController).build()

        // 테스트용 데이터 설정
        userRequest = UserRequest(
            username = "testuser",
            password = "password123"
        )

        userResponse = UserResponse(
            id = 1L,
            username = "testuser"
        )
    }

    @Test
    fun `회원가입 성공 - 200 OK 응답`() {
        `when`(userService.registerUser(any())).thenReturn(userResponse)

        mockMvc.perform(
            post("/api/users/register")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(userRequest))
        )
            .andExpect(status().isOk)
            .andExpect(jsonPath("$.id").value(1L))
            .andExpect(jsonPath("$.username").value("testuser"))

        verify(userService, times(1)).registerUser(any())
    }

    @Test
    fun `회원가입 실패 - 요청 본문 누락 (400 Bad Request)`() {
        val invalidRequest = """{"password": "password123"}""" // username 없음

        mockMvc.perform(
            post("/api/users/register")
                .contentType(MediaType.APPLICATION_JSON)
                .content(invalidRequest)
        )
            .andExpect(status().isBadRequest)
    }
}

백엔드를 배우는 입장에서, 백엔드 CRUD와 API, 그리고 테스트까지 모두 정상 작동한다는 것만으로도 감격스럽다... ㅠㅠ
상당 부분 GPT의 도움을 받아서 모든 코드가 내 것이라고 할 수는 없지만, 언제나 그랬듯 '구현'보다는 '이해와 복습'을 통해 내 것으로 만드는 데 목적을 둔 프로젝트이다.
그럼에도 포기하지 않고 어떻게든 끝까지 구현해보고 싶은 욕심이 있다.


앞으로 진행 계획

이제 프론트코드를 작성해봐야한다.

  • Next.js를 사용할 예정이다.
  • 화면 설계는 Figma를 사용할 예정이다.
  • axios를 통해 백엔드와 통신
  1. 화면 설계 (Figma)
    주요 화면을 Figma로 디자인한다.
    각 화면에서 필요한 UI 요소를 정의하고, Next.js 컴포넌트로 변환할 수 있도록 준비한다.

  2. 회원가입/로그인 페이지 구현
    axios를 사용하여 백엔드와 통신하고, 회원가입과 로그인 기능을 구현.
    로그인 후 JWT 토큰을 로컬 스토리지에 저장하여 인증을 유지한다.

  3. 게시판 페이지 구현
    게시글 목록을 가져오는 API를 호출하여 화면에 게시글을 표시한다.
    게시글 클릭 시, 해당 게시글의 상세 정보를 보여주는 페이지를 구현한다.

  4. 댓글 기능 구현
    댓글 작성 및 조회 기능을 구현한다.
    댓글은 게시글 상세 페이지에서 작성할 수 있게 된다.

  5. 배포 및 최종 테스트
    백엔드: Spring Boot 애플리케이션을 빌드하여 서버에 배포한다.
    프론트엔드: Next.js 애플리케이션을 빌드하고 배포한다.
    로컬 및 테스트 환경에서 모든 기능이 정상적으로 작동하는지 확인한다.

0개의 댓글