비즈니스 로직 구현 단계.
interface로 구현해 어떤 argument를 받고 어떤 response를 보낸다.
실질적으로 뒤에서 구현. 근데 사실 하나의 인터페이스에서 여러 서비스 구현체가 나올일 이 없어 클래스로 바로
구현체를 작성한다.
CourseController 가 CourseService를 호출한다. 컨트롤러에서 CRUD를 작성했으니 서비스 단계에서도 CRUD를 이용해 비즈니스 로직을 실행한다. -> 그리고 다시 Response DTO를 return 하면 된다.
컨트롤러 메서드에 썼던 @Path Variable 인자를 서비스 메서드들에도 똑같이 써줘야 한다.
package com.teamsparta.courseregistration.domain.course.service
import com.teamsparta.courseregistration.domain.course.dto.CourseResponse
import com.teamsparta.courseregistration.domain.course.dto.CreateCourseRequest
import com.teamsparta.courseregistration.domain.course.dto.UpdateCourseRequest
interface CourseService {
fun getAllCourseList(): List<CourseResponse>
fun getOneCourseById(courseId: Long): CourseResponse
fun createCourse(request: CreateCourseRequest): CourseResponse
fun updateCourse(courseId: Long, request: UpdateCourseRequest): CourseResponse
fun deleteCourse(courseId: Long)
}
package com.teamsparta.courseregistration.domain.course.service
import com.teamsparta.courseregistration.domain.course.dto.CourseResponse
import com.teamsparta.courseregistration.domain.course.dto.CreateCourseRequest
import com.teamsparta.courseregistration.domain.course.dto.UpdateCourseRequest
import org.springframework.stereotype.Service
@Service // 직관적으로 Service 단계인것을 명시
class CourseServiceImpl: CourseService {
override fun getAllCourseList(): List<CourseResponse> {
// TODO: DB에서 모든 Course 목록(Entity)을 CourseResponse 목록으로 변환 후 반환
TODO("Not yet implemented")
}
override fun getOneCourseById(courseId: Long): CourseResponse {
// TODO: DB에서 ID기반으로 Course(Entity) 가져와서 CourseResponse 변환 후 반환
TODO("Not yet implemented")
}
override fun createCourse(request: CreateCourseRequest): CourseResponse {
// TODO: request를 Course(Entity)로 변환 후 DB에 저장
TODO("Not yet implemented")
}
override fun updateCourse(courseId: Long, request: UpdateCourseRequest): CourseResponse {
// TODO: DB에서 courseId에 해당하는 Course(Entity)를 가져와서 request 기반으로 업데이트 후 DB에 저장, 결과를 CourseResponse(데이터 클래스 프로퍼티 형식)로 변환 후 반환
TODO("Not yet implemented")
}
override fun deleteCourse(courseId: Long) {
// TODO: DB에서 courseId에 해당하는 Course(Entity)를 삭제, 연관된 CourseApplication, Lecture 모두 삭제
TODO("Not yet implemented")
}
}
이제 Controller와 연결하기 !!
CourseController에서 CourseService를 주입 받아야 한다.
@RequestMapping("/courses")
@RestController
class CourseController(
private val courseService: CourseService // 인터페이스만 주입해도 Spring에서 알아서 CourseService를 상속받는 @Service 어노테이션이 붙은 Bean들을 찾아주기 때문에 이런식으로 사용 가능.
) {
@GetMapping()
fun getCourseList(): ResponseEntity<List<CourseResponse>> { // CourseResponse 하나하나를 리스트에 담아 Array 형태로 get.
return ResponseEntity
.status(HttpStatus.OK)
.body(courseService.getAllCourseList()) // body로 DB에서 모든 Course 목록을 CourseResponse 목록으로 변환한 것이 담긴다.
// body에 DTO가 담시는 것.
}
// 단일 코스 하나만 가져오기
@GetMapping("/{courseId}") // 여기에 표기한 PathVariable과 메서드 인자로 받는 PathVariable 네이밍 일치 시킴.
fun getCourse(@PathVariable cousreId: Long): ResponseEntity<CourseResponse> { // CourseResponse에서 하나만 id값에 기반해 하나만 get.
return ResponseEntity
.status(HttpStatus.OK)
.body(courseService.getOneCourseById(cousreId))
}
@PostMapping
fun createCourse(@RequestBody createCourseRequest: CreateCourseRequest): ResponseEntity<CourseResponse> {
// JSON이 CreateCourseRequest 데이터 클래스의 DTO로 매핑된다.
// 생성을 하면 생선된 Response를 줘야함. ResponseEntity는 Spring 프레임워크에서 상세한 Response 객체를 구성하기 위해 제공하는 기능. 이걸 쓰는 이유는 DTO 뿐만이 아니라 statusCode도 같이 줘야하기 때문에.
// 이 꺽쇠 안에는 CourseResponse란 DTO를 넣어 주면 됨.
return ResponseEntity
.status(HttpStatus.CREATED) //CREATED 201 반환
.body(courseService.createCourse(createCourseRequest))
}
@PutMapping("/{courseId}")
fun updateCourse(
@PathVariable courseId: Long,
@RequestBody updateCourseRequest: UpdateCourseRequest
): ResponseEntity<CourseResponse> {
return ResponseEntity
.status(HttpStatus.OK)
.body(courseService.updateCourse(courseId, updateCourseRequest))
}
@DeleteMapping("/{courseId}")
fun deleteCourse(@PathVariable courseId: Long): ResponseEntity<Unit> {
return ResponseEntity
.status(HttpStatus.NO_CONTENT)
// .body(courseService.deleteCourse(courseId)) // 이렇게 쓰면 오류남
.build() // body를 굳이 표현하지 않을 때 빌드를 씀
}
}
Transactional : 모델이 생성되거나(Create), 수정되거나(Update), 삭제 될 때(Delete) 오류가 난다면 롤백하기
Transactional 단위 지정하기
package com.teamsparta.courseregistration.domain.course.service
import com.teamsparta.courseregistration.domain.course.dto.CourseResponse
import com.teamsparta.courseregistration.domain.course.dto.CreateCourseRequest
import com.teamsparta.courseregistration.domain.course.dto.UpdateCourseRequest
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service // 직관적으로 Service 단계인것을 명시
class CourseServiceImpl: CourseService {
override fun getAllCourseList(): List<CourseResponse> {
// TODO: DB에서 모든 Course 목록(Entity)을 CourseResponse 목록으로 변환 후 반환
TODO("Not yet implemented")
}
override fun getOneCourseById(courseId: Long): CourseResponse {
// TODO: 만약 courseId에 해당하는 Course가 없다면 만든 오류 던지기 throw ModelNotFoundException
// TODO: DB에서 ID기반으로 Course(Entity) 가져와서 CourseResponse 변환 후 반환
TODO("Not yet implemented")
}
@Transactional
override fun createCourse(request: CreateCourseRequest): CourseResponse {
// TODO:
// TODO: request를 Course(Entity)로 변환 후 DB에 저장
TODO("Not yet implemented")
}
@Transactional
override fun updateCourse(courseId: Long, request: UpdateCourseRequest): CourseResponse {
// TODO: 만약 courseId에 해당하는 Course가 없다면 만든 오류 던지기 throw ModelNotFoundException
// TODO: DB에서 courseId에 해당하는 Course(Entity)를 가져와서 request 기반으로 업데이트 후 DB에 저장, 결과를 CourseResponse(데이터 클래스 프로퍼티 형식)로 변환 후 반환
TODO("Not yet implemented")
}
@Transactional
override fun deleteCourse(courseId: Long) {
// TODO: 만약 courseId에 해당하는 Course가 없다면 만든 오류 던지기 throw ModelNotFoundException
// TODO: DB에서 courseId에 해당하는 Course(Entity)를 삭제, 연관된 CourseApplication, Lecture 모두 삭제
TODO("Not yet implemented")
}
}
트랜잭셔널 사용하려면 DB랑 연결해야 한다. 그래서 테스트 DB에 연결.
하지만 각 Controller마다 Exception에 대한 핸들링을 하다보니, 중복코드가 많이 생길 수 있다는 단점이 있다.
이를 위해, Spring에서는 Exception을 전역적으로 처리할 수 있도록 @ControllerAdvice, @RestControllerAdvice 어노테이션을 제공.
앞에서 했던것처럼, view가 아닌 data만을 다루기에 @RestControllerAdvice을 이용!
해당 Annotation을 Class에 지정한 후, @ExceptionHandler를 통해 전역적으로 Exception을 핸들링 할 수 있다.
package com.teamsparta.courseregistration.domain.exception.dto
data class ErrorResponse(
val message: String?,
)
package com.teamsparta.courseregistration.domain.exception
// 다른 모델들 모두에게 예외를 던져주기 위해서 domain 파일 하위에 새로운 exception 패키지 생성
// 예외를 단순히 던지기만 할 것이기 때문에 데이터 클래스 생성
// 이 예외를 보고서 어떤 모델의 어떤 ID에서 NotFoundException이 발생했는지 알 수 있다.
// Exception의 종류 중 하나로 메시지(String)와 throw된 원인(cause)을 리턴해준다.
data class ModelNotFoundException(val modelName: String, val id: Long): RuntimeException(
"Model $modelName not found with given id: $id"
)
package com.teamsparta.courseregistration.domain.exception
import com.teamsparta.courseregistration.domain.exception.dto.ErrorResponse
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
//이렇게 exception 패키지 내 클래스 생성. 전략적으로 한 곳에서 Exception을 처리할 수 있다.
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(ModelNotFoundException::class) // 만든 data class ModelNotFoundException
fun handleModelNotFoundException(e: ModelNotFoundException): ResponseEntity<ErrorResponse> { // status 코드만 리턴 받을 것임
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse(message = e.message))
}
}
단점: CourseService가 너무 커진다.
package com.teamsparta.courseregistration.domain.course.service
import com.teamsparta.courseregistration.domain.course.dto.CourseResponse
import com.teamsparta.courseregistration.domain.course.dto.CreateCourseRequest
import com.teamsparta.courseregistration.domain.course.dto.UpdateCourseRequest
import com.teamsparta.courseregistration.domain.courseapplication.dto.ApplyCourseRequest
import com.teamsparta.courseregistration.domain.courseapplication.dto.CourseApplicationResponse
import com.teamsparta.courseregistration.domain.courseapplication.dto.UpdateApplicationStatusRequest
import com.teamsparta.courseregistration.domain.lecture.dto.AddLectureRequest
import com.teamsparta.courseregistration.domain.lecture.dto.LectureResponse
import com.teamsparta.courseregistration.domain.lecture.dto.UpdateLectureRequest
interface CourseService {
fun getAllCourseList(): List<CourseResponse>
fun getOneCourseById(courseId: Long): CourseResponse
fun createCourse(request: CreateCourseRequest): CourseResponse
fun updateCourse(courseId: Long, request: UpdateCourseRequest): CourseResponse
fun deleteCourse(courseId: Long)
fun getAllLectureList(courseId: Long): List<LectureResponse>
fun getOneLecture(courseId: Long, lectureId: Long): LectureResponse
fun addLecture(courseId: Long, request: AddLectureRequest): LectureResponse
fun updateLecture(courseId: Long, lectureId: Long, request: UpdateLectureRequest): LectureResponse
fun removeLecture(courseId: Long, lectureId: Long)
fun getCourseApplicationList(courseId: Long): List<CourseApplicationResponse>
fun getOneCourseApplication(courseId: Long, applicationId: Long): CourseApplicationResponse
fun applyCourse(courseId: Long, request: ApplyCourseRequest): CourseApplicationResponse
fun updateApplicationStatus(
courseId: Long,
applicationId: Long,
request: UpdateApplicationStatusRequest
): CourseApplicationResponse
}
package com.teamsparta.courseregistration.domain.course.service
import com.teamsparta.courseregistration.domain.course.dto.CourseResponse
import com.teamsparta.courseregistration.domain.course.dto.CreateCourseRequest
import com.teamsparta.courseregistration.domain.course.dto.UpdateCourseRequest
import com.teamsparta.courseregistration.domain.courseapplication.dto.ApplyCourseRequest
import com.teamsparta.courseregistration.domain.courseapplication.dto.CourseApplicationResponse
import com.teamsparta.courseregistration.domain.courseapplication.dto.UpdateApplicationStatusRequest
import com.teamsparta.courseregistration.domain.exception.ModelNotFoundException
import com.teamsparta.courseregistration.domain.lecture.dto.AddLectureRequest
import com.teamsparta.courseregistration.domain.lecture.dto.LectureResponse
import com.teamsparta.courseregistration.domain.lecture.dto.UpdateLectureRequest
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service // 직관적으로 Service 단계인것을 명시
class CourseServiceImpl: CourseService {
override fun getAllCourseList(): List<CourseResponse> {
// TODO: DB에서 모든 Course 목록(Entity)을 CourseResponse 목록으로 변환 후 반환
TODO("Not yet implemented")
}
override fun getOneCourseById(courseId: Long): CourseResponse {
// TODO: 만약 courseId에 해당하는 Course가 없다면 만든 오류 던지기 throw ModelNotFoundException
// TODO: DB에서 ID기반으로 Course(Entity)를 가져와서 CourseResponse 변환 후 반환
// TODO("Not yet implemented")
throw ModelNotFoundException(modelName = "Course", id = 3L)
}
@Transactional
override fun createCourse(request: CreateCourseRequest): CourseResponse {
// TODO:
// TODO: request를 Course(Entity)로 변환 후 DB에 저장
TODO("Not yet implemented")
}
@Transactional
override fun updateCourse(courseId: Long, request: UpdateCourseRequest): CourseResponse {
// TODO: 만약 courseId에 해당하는 Course가 없다면 만든 오류 던지기 throw ModelNotFoundException
// TODO: DB에서 courseId에 해당하는 Course(Entity)를 가져와서 request 기반으로 업데이트 후 DB에 저장, 결과를 CourseResponse(데이터 클래스 프로퍼티 형식)로 변환 후 반환
// TODO("Not yet implemented")
throw ModelNotFoundException(modelName = "Course", id = 3L)
}
@Transactional
override fun deleteCourse(courseId: Long) {
// TODO: 만약 courseId에 해당하는 Course가 없다면 만든 오류 던지기 throw ModelNotFoundException
// TODO: DB에서 courseId에 해당하는 Course(Entity)를 삭제, 연관된 CourseApplication, Lecture 모두 삭제
// TODO("Not yet implemented")
throw ModelNotFoundException(modelName = "Course", id = 3L)
}
override fun getAllLectureList(courseId: Long): List<LectureResponse> {
// TODO: 만약 courseId에 해당하는 Course가 없다면 throw ModelNotFoundException
// TODO: DB에서 courseId에 해당하는 Course목록을 가져오고, 하위 lecture들을 가져온 다음, LectureResopnse로 변환해서 반환
TODO("Not yet implemented")
}
override fun getOneLecture(courseId: Long, lectureId: Long): LectureResponse {
// TODO: 만약 courseId, lectureId에 해당하는 Lecture가 없다면 throw ModelNotFoundException
// TODO: DB에서 courseId, lectureId에 해당하는 Lecture를 가져와서 LectureResponse로 변환 후 반환
TODO("Not yet implemented")
}
@Transactional
override fun addLecture(courseId: Long, request: AddLectureRequest): LectureResponse {
// TODO: 만약 courseId에 해당하는 Course가 없다면 throw ModelNotFoundException
// TODO: DB에서 courseId에 해당하는 Course를 가져와서 Lecture를 추가 후 DB에 저장, 결과를을 LectureResponse로 변환 후 반환
TODO("Not yet implemented")
}
@Transactional
override fun updateLecture(courseId: Long, lectureId: Long, request: UpdateLectureRequest): LectureResponse {
// TODO: 만약 courseId, lectureId에 해당하는 Lecture가 없다면 throw ModelNotFoundException
/* TODO: DB에서 courseId, lectureId에 해당하는 Lecture를 가져와서
request로 업데이트 후 DB에 저장, 결과를을 LectureResponse로 변환 후 반환 */
TODO("Not yet implemented")
}
@Transactional
override fun removeLecture(courseId: Long, lectureId: Long) {
// TODO: 만약 courseId에 해당하는 Course가 없다면 throw ModelNotFoundException
// TODO: DB에서 courseId, lectureId에 해당하는 Lecture를 가져오고, 삭제
TODO("Not yet implemented")
}
override fun getCourseApplicationList(courseId: Long): List<CourseApplicationResponse> {
// TODO: 만약 courseId에 해당하는 Course가 없다면 throw ModelNotFoundException
// TODO: DB에서 courseId에 해당하는 Course를 가져오고, 하위 courseApplication들을 CourseApplicationResponse로 변환 후 반환
TODO("Not yet implemented")
}
override fun getOneCourseApplication(courseId: Long, applicationId: Long): CourseApplicationResponse {
// TODO: 만약 courseId, applicationId에 해당하는 CourseApplication이 없다면 throw ModelNotFoundException
// TODO: DB에서 courseId, applicationId에 해당하는 CourseApplication을 가져와서 CourseApplicationResponse로 변환 후 반환
TODO("Not yet implemented")
}
// 정책 부분
@Transactional
override fun applyCourse(courseId: Long, request: ApplyCourseRequest): CourseApplicationResponse {
// TODO: 만약 courseId에 해당하는 Course가 없다면 throw ModelNotFoundException
// TODO: 만약 course가 이미 마감됐다면, throw IllegalStateException // 마감됐다는 것이 state 가 illegal 하다는 것과 동일.
// TODO: 이미 신청했다면, throw IllegalStateException
TODO("Not yet implemented")
}
// 정책 부분
@Transactional
override fun updateApplicationStatus(
courseId: Long,
applicationId: Long,
request: UpdateApplicationStatusRequest
): CourseApplicationResponse {
// TODO: 만약 courseId, applicationId에 해당하는 CourseApplication이 없다면 throw ModelNotFoundException
// TODO: 만약 status가 이미 변경된 상태면 throw IllegalStateException
// TODO: Course의 status가 CLOSED상태 일시 throw IllegalStateException
// TODO: 승인을 하는 케이스일 경우, course의 numApplicants와 maxApplicants가 동일하면, course의 상태를 CLOSED로 변경
// TODO: DB에서 courseApplication을 가져오고, status를 request로 업데이트 후 DB에 저장, 결과를 CourseApplicationResponse로 변환 후 반환
TODO("Not yet implemented")
}
}
package com.teamsparta.courseregistration.domain.courseapplication.controller
import com.teamsparta.courseregistration.domain.course.service.CourseService
import com.teamsparta.courseregistration.domain.courseapplication.dto.ApplyCourseRequest
import com.teamsparta.courseregistration.domain.courseapplication.dto.CourseApplicationResponse
import com.teamsparta.courseregistration.domain.courseapplication.dto.UpdateApplicationStatusRequest
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@RequestMapping("/course/{courseId}/applications")
@RestController
class CourseApplicationController(
private val courseService: CourseService
) {
@GetMapping
fun getApplicationList(@PathVariable courseId: Long): ResponseEntity<List<CourseApplicationResponse>> {
return ResponseEntity
.status(HttpStatus.OK)
.body(courseService.getCourseApplicationList(courseId))
}
@GetMapping("/{applicationId}")
fun getApplication(
@PathVariable courseId: Long,
@PathVariable applicationId: Long,
): ResponseEntity<CourseApplicationResponse> {
return ResponseEntity
.status(HttpStatus.OK)
.body(courseService.getOneCourseApplication(courseId, applicationId))
}
@PostMapping
fun applyCourse(
@PathVariable courseId: Long,
@RequestBody applyCourseRequest: ApplyCourseRequest
): ResponseEntity<CourseApplicationResponse> {
return ResponseEntity
.status(HttpStatus.CREATED)
.body(courseService.applyCourse(courseId, applyCourseRequest))
}
@PatchMapping("/{applicationId}")
fun updateApplicationStatus(
@PathVariable courseId: Long,
@PathVariable applicationId: Long,
@RequestBody updateApplicationStatusRequest: UpdateApplicationStatusRequest
): ResponseEntity<CourseApplicationResponse> {
return ResponseEntity
.status(HttpStatus.OK)
.body(courseService.updateApplicationStatus(courseId, applicationId, updateApplicationStatusRequest))
}
}
package com.teamsparta.courseregistration.domain.lecture.controller
import com.teamsparta.courseregistration.domain.course.service.CourseService
import com.teamsparta.courseregistration.domain.lecture.dto.AddLectureRequest
import com.teamsparta.courseregistration.domain.lecture.dto.LectureResponse
import com.teamsparta.courseregistration.domain.lecture.dto.UpdateLectureRequest
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
// 컨트롤러에 서비스 연결
@RequestMapping("/courese/{courseId}/lectures") // 여기서 @PathVariable로 courseId를 썼기 때문에 모든 함수 내 @PathVariable 자리에 courseId 지정해야 함.
@RestController // @RequestMapping이 있다 보니까
class LectureController(
private val courseService: CourseService
) {
@GetMapping
fun getLectureList(@PathVariable courseId: Long): ResponseEntity<List<LectureResponse>> {
return ResponseEntity
.status(HttpStatus.OK)
.body(courseService.getAllLectureList(courseId))
}
@GetMapping("/{lectureId}")
fun getLecture(@PathVariable courseId: Long, @PathVariable lectureId: Long): ResponseEntity<LectureResponse> {
return ResponseEntity
.status(HttpStatus.OK)
.body(courseService.getOneLecture(courseId, lectureId))
}
@PostMapping
fun addLecture(
@PathVariable courseId: Long,
@RequestBody addLectureRequest: AddLectureRequest
): ResponseEntity<LectureResponse> { // 완성을 했으면 lecture가 어떤 데이터로 생성이 됐는지 Resonse를 보내야 하니까 ResponseEntity에 AddLectureRequest를 지정해서 리턴 시키는 것.
return ResponseEntity
.status(HttpStatus.CREATED)
.body(courseService.addLecture(courseId, addLectureRequest))
}
@PutMapping("/{lectureId}") // 이미 생성된 강좌이니 수정할 때 강좌 아이디가 필요함
fun updateLecture(
@PathVariable courseId: Long,
@PathVariable lectureId: Long,
@RequestBody updateLectureRequest: UpdateLectureRequest
): ResponseEntity<LectureResponse> {
return ResponseEntity
.status(HttpStatus.OK)
.body(courseService.updateLecture(courseId, lectureId, updateLectureRequest))
}
@DeleteMapping("/{lectureId}")
fun removeLecture(
@PathVariable courseId: Long,
@PathVariable lectureId: Long,
): ResponseEntity<Unit> { // 아무것도 리턴하지 않는다는 뜻
return ResponseEntity
.status(HttpStatus.NO_CONTENT)
.build()
}
}
Controller와 Service가 모두 연결됐다.
@Transactional : 예외가 있으면 삭제가 안되고 롤백이 되게한다.