github : https://github.com/BaekGeunYoung/performance_reservation_app

최근 spring-boot 와 kotlin을 공부하고 있는데, 기본적인 프로젝트 설정부터 controller, repository, service, dto 등의 개념을 모두 종합해 완결성 있는 백엔드 어플리케이션을 만들어보아야겠다는 생각에 진행한 실습이다.

Application Structure


image.png

config

  1. Jwt config for generating & validating jwt token
  2. Security Config for user authentication & authorization
  3. Bean Config for common Beans

controller

request를 input으로 받아 사용자가 원하는 output를 response로 돌려주는 layer
실제 작업은 service layer에게 delegate하고 process의 커다란 flow만을 제어함.

dto

entity layer에 1대1 mapping 되는 object로, request <-> controller, controller <-> service, controller <-> response 등의
layer간 데이터 교환을 위한 object

entity

실제 DB에 담기는 data와 mapping 되는 object.

repository

entity에 직접적으로 접근하는 DAO들을 담고있는 디렉토리. JpaRepository interface를 implement함으로써 구현됨.

service

controller부터 작업의 책임을 넘겨받아 실제 business logic을 구현하는 layer.
여기서는 business logic만을 구현하고, 실제 데이터에 접근하는 것은 repository layer에게 delegate함.

JWT

JWT를 이용한 사용자 인증을 구현하기 위해 다음과 같은 기능이 필요함.

  1. request header에 token이 실려왔을 때, 이를 resolve해 사용자 정보에 접근하기
  2. login에 성공할 시 새로운 token 발급하기

1번 feature는 security configuration에서 jwt에 관한 custom filter를 적용함으로써 구현할 수 있음.

class JwtTokenFilter(
        private val jwtTokenProvider: JwtTokenProvider
) : GenericFilterBean() {
    override fun doFilter(request: ServletRequest?, response: ServletResponse?, chain: FilterChain?) {
        request ?: throw Exception()
        val token: String? = jwtTokenProvider.resolveToken(request as HttpServletRequest)
        token?.let {
            if(jwtTokenProvider.validateToken(token)) {
                val auth: Authentication = jwtTokenProvider.getAuthentication(token)
                SecurityContextHolder.getContext().authentication = auth
            }
        }
        chain?.doFilter(request, response)
    }
}

위와 같이 filter를 생성하고, 이를 httpSecurity builder의 addFilterBefore함수를 이용해 적용해줄 수 있음.

2번 feature는 로그인 요청을 처리하는 userController class에 jwtTokenProvider를 autowired annotation으로 주입해줌으로써
로그인에 성공했을 시 토큰을 발급하는 과정을 추가할 수 있음.

@PostMapping("/login")
    fun login(@RequestBody @Valid request: UserLoginRequest): MutableMap<String, Any> {
        try{
            val username: String = request.username
            val password: String = request.password
            val authenticator = UsernamePasswordAuthenticationToken(username, password)
            authenticationManager.authenticate(authenticator)
            val token: String = jwtTokenProvider.createToken(username, listOf("ADMIN"))

            val model: MutableMap<String, Any> = HashMap()
            model["username"] = request.username
            model["token"] = token

            return model
        }
        catch (e: AuthenticationException) {
            throw BadCredentialsException("invalid username/password supplied")
        }
    }

simulation


simulation scenario

  1. 회원가입
  2. 로그인
  3. 내 정보 확인
  4. 공연 등록
  5. 좌석 예매
  6. 좌석 정보 열람

1. 회원가입

request :
image.png

response :
image.png

(id에 null 들어가있는 거 수정 필요)

--> 정상적으로 user entity가 생성되고, bcryptPasswordEncoder를 security config에서 지정해주었으므로 비밀번호가 encoding되어서 정보가 저장된다.

2. 로그인

request :
image.png

response :
image.png

login 관련 controller에서 작성한대로 로그인 요청이 성공할 시 유저 정보를 담고 있는 jwt token을 발급해주도록 함.

image.png

잘못된 정보를 보낼 시 의도한 대로 badCredentialsException 발생

3. 내 정보 확인

image.png

로그인 시 발급받았던 토큰을 헤더에 실어 /api/v1/user/me endpoint로 get 요청 시 본인에 대한 정보를 받을 수 있음.
security config에서 filter를 통해 token을 resolve해 그 내용을 authentication 객체에 주입해 줌.

4. 공연 등록

image.png

이후 시간이 겹치도록 공연 등록을 시도하면 badCredentialsException 발생
image.png

image.png

5. 좌석 예매

image.png

원하는 공연의 id와 좌석번호를 주면 좌석 예매가능. 이미 예매된 좌석을 예매하려고 시도할 시 에러 발생
image.png

6. 좌석정보 열람

1) 좌석 id를 통해 접근
image.png

2) 자기가 예매한 좌석 리스트
image.png