에 들어가서 CrudRepository를 클릭하면, 쓸수 있는 함수들을 볼수있다.
자세히 보면, `findAll()` 을 통해 모든 데이터를 가져올 수 있고, `findAllById` 를 통해 특정 ID 목록에 해당하는 Entity 목록을 가져올 수 있는걸 알 수 있다. `SELECT * FROM ~` , `SELECT * FROM ~ WHERE id IN ()` 과 동일하다.예시는
val allCourses = courseRepository.findAll()
val specificCourses = courseRepository.findAllById(listOf(1L, 2L, 3L))
그 다음, `findById()` 가 보인다. id에 기반해 한개의 데이터를 리턴한다. 그리고 이건 굉장히 많이 쓰이는 함수이다.
그런데, Optional<T> findById(ID id) 즉, Optional<T>` 로 되어있는데, Kotlin에서는 굳이 Optional을 쓸 이유가 없다.
이 때문에, Spring Boot 2.1.2 버전부터 `findByIdOrNull` 함수를 추가로 지원한다. 이것은 null 일시 exception을 던질 수 있고, 이후의 코드에서는 course가 null이 아니라는 걸 보장할 수 있다.
val course = courseRepository.findByIdOrNull(1L) ?: throw ModelNotFoundException("Course", 1L)
-그리고, `save()` 와 `saveAll()` 은 Entity를 추가, 혹은 수정을 한다. 영속성 컨텍스트에 등록하여 변경을 감지하고, 트랜잭션이 종료될 때 변경사항을 데이터베이스에 반영한다.
실제 구현부를 보면, 새로운 entity 면 `em.persist()` , 그렇지 않은 상태(detached)면 `em.merge()` 를 호출하는 걸 볼 수 있다. 우리는 이렇게 EntityManager를 직접 쓰지 않고도, EntityManager의 기능을 추상화된 Repository 라는 인터페이스를 통해 쉽게 쓸 수 있다
public <S extends T> S save(S entity) {
if (this.entityInformation.isNew(entity)) {
this.em.persist(entity);
return entity;
} else {
return this.em.merge(entity);
}
}
마지막으로, 삭제를 담당하는 `delete()`, `deleteAll()` 도 데이터의 존재 여부를 확인 하는`existsById()` Method도 볼 수 있다.
Repository 구현하기
course 패키지 하위에 repository 를 추가한후, CourseRepository Interface를 작성, JpaRepository<Course, Long> 을 상속받으면 됨. 그걸위해 3개를 작성해야함.
JpaRepository의 명세를 보면, JpaRepository<T, ID> 형태로 되어있으므로, 앞에는 Entity의 type, 뒤에는 id의 Type을 설정하면 된다.
import com.example.courseregistration.domain.course.model.Course
import org.springframework.data.jpa.repository.JpaRepository
interface CourseRepository: JpaRepository<Course, Long> {
}
이제 이걸 interface CourseRepository: JpaRepository<Course, Long> 색칠 부분만 바꿔서 유저, 코스어플,렉쳐에 추가하면 된다.
sql함수 있는 사이트
https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html
. find 는 SELECT 에 해당하고, By 는 WHERE. 에를 들자면,
findDistinctByLastnameAndFirstname | select distinct … where x.lastname = ?1 and x.firstname = ?2 |
query를 직접 작성하여 Method에 맵핑
항상 별칭을 사용해야하고, 대소문자를 구분하기에 Table name대신에 대문자로 시작하는 Entity name을 사용해야합니다. 예를들어 select * from post 형태가 아닌, select p from Post p 를 사용합니다.
query에 변수를 넣어줄 때에는, : 을 앞에 붙여 사용하고, @Param Annotation을 사용하여 해당 변수와 맵핑되는 Argument를 지정해줍니다. 위의 Method 이름을 통해 작성한 쿼리는 아래 처럼 표현될 수 있습니다.
interface PostRepository: JpaRepository<Post, Long> {
@Query("select p from Post p where p.title like %:title%)
fun searchByTitle(@Param("title") keyword: String): List<Post>
}
val course = courseRepository.findByIdOrNull(1L) ?: throw ModelNotFoundException("Course", 1L)
val courseResponse = CourseResponse(
id = course.id!!,
title = course.title,
description = course.description,
status = course.status.name,
maxApplicants = course.maxApplicants,
numApplicants = course.numApplicants
)
return courseResponse
- 하지만, 매번 이렇게 변환을 하기에는 코드의 중복이 너무 많아집니다. 우리는 이를 위해, 변환하는 함수를 작성해 줄 수 있습니다. Course Class 하위에 아래처럼 Extension Function을 추가해줄 수 있습니다! DTO로 변환하는 것은 Model이 가지는 필수적인 도메인 로직이 아니기 때문에, Extension Function을 활용하면 좋습니다.
- fun Course.toResponse(): CourseResponse { return CourseResponse( id = id!!, title = title, description = description, status = status.name, maxApplicants = maxApplicants, numApplicants = numApplicants ) }
최종적으로, 처음에 작성한 코드는 아래처럼 단순화됩니다!
- val course = courseRepository.findByIdOrNull(1L) ?: throw ModelNotFoundException("Course", 1L) return course.toResponse()
작성한 코드의 최종형태
코스
@Entity
@Table(name = "course")
class Course(
@Column(name = "title", nullable = false)
var title: String,
@Column(name = "description")
var description: String? = null,
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
var status: CourseStatus = CourseStatus.OPEN,
@Column(name = "max_applicants", nullable = false)
val maxApplicants: Int = 30,
@Column(name = "num_applicants", nullable = false)
var numApplicants: Int = 0,
@OneToMany(mappedBy = "course", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY)
var lectures: MutableList<Lecture> = mutableListOf(),
@OneToMany(mappedBy = "course", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY)
var courseApplications: MutableList<CourseApplication> = mutableListOf()
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
fun isFull(): Boolean {
return numApplicants >= maxApplicants
}
fun isClosed(): Boolean {
return status == CourseStatus.CLOSED
}
fun close() {
status = CourseStatus.CLOSED
}
fun addApplicant() {
numApplicants += 1
}
fun addLecture(lecture: Lecture) {
lectures.add(lecture)
}
fun removeLecture(lecture: Lecture) {
lectures.remove(lecture)
}
fun addCourseApplication(courseApplication: CourseApplication) {
courseApplications.add(courseApplication)
}
}
fun Course.toResponse(): CourseResponse {
return CourseResponse(
id = id!!,
title = title,
description = description,
status = status.name,
maxApplicants = maxApplicants,
numApplicants = numApplicants
)
}
코스리포짓
interface CourseRepository: JpaRepository<Course, Long> {
}
유저
@Entity
@Table(name = "app_user")
class User(
@Column(name = "email", nullable = false)
val email: String,
@Column(name = "password", nullable = false)
val password: String,
@Embedded
var profile: Profile,
@Enumerated(EnumType.STRING)
@Column(name = "role", nullable = false)
val role: UserRole,
@OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY)
val courseApplications: MutableList<CourseApplication> = mutableListOf()
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
}
fun User.toResponse(): UserResponse {
return UserResponse(
id = id!!,
nickname = profile.nickname,
email = email,
role = role.name
)
}
코스어플
@Entity
@Table(name = "course_application")
class CourseApplication(
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
var status: CourseApplicationStatus = CourseApplicationStatus.PENDING,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "course_id")
val course: Course,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
val user: User
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
fun isProceeded(): Boolean {
return status != CourseApplicationStatus.PENDING
}
fun accept() {
status = CourseApplicationStatus.ACCEPTED
}
fun reject() {
status = CourseApplicationStatus.REJECTED
}
}
fun CourseApplication.toResponse(): CourseApplicationResponse {
return CourseApplicationResponse(
id = id!!,
course = course.toResponse(),
user = user.toResponse(),
status = status.name
)
}
코스어플리포짓
interface CourseApplicationRepository: JpaRepository<CourseApplication, Long> {
fun existsByCourseIdAndUserId(courseId: Long, userId: Long): Boolean
fun findByCourseIdAndId(courseId: Long, id: Long): CourseApplication?
}
유저
@Entity
@Table(name = "app_user")
class User(
@Column(name = "email", nullable = false)
val email: String,
@Column(name = "password", nullable = false)
val password: String,
@Embedded
var profile: Profile,
@Enumerated(EnumType.STRING)
@Column(name = "role", nullable = false)
val role: UserRole,
@OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY)
val courseApplications: MutableList<CourseApplication> = mutableListOf()
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
}
fun User.toResponse(): UserResponse {
return UserResponse(
id = id!!,
nickname = profile.nickname,
email = email,
role = role.name
)
}
유저서비스임플
@Service
class UserServiceImpl(
private val userRepository: UserRepository
): UserService {
@Transactional
override fun signUp(request: SignUpRequest): UserResponse {
if (userRepository.existsByEmail(request.email)) {
throw IllegalStateException("Email is already in use")
}
return userRepository.save(
User(
email = request.email,
// TODO: 비밀번호 암호화
password = request.password,
profile = Profile(
nickname = request.nickname
),
role = when (request.role) {
UserRole.STUDENT.name -> UserRole.STUDENT
UserRole.TUTOR.name -> UserRole.TUTOR
else -> throw IllegalArgumentException("Invalid role")
}
)
).toResponse()
}
@Transactional
override fun updateUserProfile(userId: Long, request: UpdateUserProfileRequest): UserResponse {
val user = userRepository.findByIdOrNull(userId) ?: throw ModelNotFoundException("User", userId)
user.profile = Profile(
nickname = request.nickname
)
return userRepository.save(user).toResponse()
}
}
유저리포짓
interface UserRepository: JpaRepository<User, Long> {
fun existsByEmail(email: String): Boolean
}
렉쳐
@Entity
@Table(name = "lecture")
class Lecture(
@Column(name = "title", nullable = false)
var title: String,
@Column(name = "video_url", nullable = false)
var videoUrl: String,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "course_id")
var course: Course
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
}
fun Lecture.toResponse(): LectureResponse {
return LectureResponse(
id = id!!,
title = title,
videoUrl = videoUrl,
)
}
렉쳐리포짓
interface LectureRepository: JpaRepository<Lecture, Long> {
fun findByCourseIdAndId(courseId: Long, lectureId: Long): Lecture?
}
코스서비스임플
@Service
class CourseServiceImpl(
private val courseRepository: CourseRepository,
private val lectureRepository: LectureRepository,
private val courseApplicationRepository: CourseApplicationRepository,
private val userRepository: UserRepository,
) : CourseService {
override fun getAllCourseList(): List<CourseResponse> {
return courseRepository.findAll().map { it.toResponse() }
}
override fun getCourseById(courseId: Long): CourseResponse {
val course = courseRepository.findByIdOrNull(courseId) ?: throw ModelNotFoundException("Course", courseId)
return course.toResponse()
}
@Transactional
override fun createCourse(request: CreateCourseRequest): CourseResponse {
return courseRepository.save(
Course(
title = request.title,
description = request.description,
status = CourseStatus.OPEN,
)
).toResponse()
}
@Transactional
override fun updateCourse(courseId: Long, request: UpdateCourseRequest): CourseResponse {
val course = courseRepository.findByIdOrNull(courseId) ?: throw ModelNotFoundException("Course", courseId)
val (title, description) = request
course.title = title
course.description = description
return courseRepository.save(course).toResponse()
}
@Transactional
override fun deleteCourse(courseId: Long) {
val course = courseRepository.findByIdOrNull(courseId) ?: throw ModelNotFoundException("Course", courseId)
courseRepository.delete(course)
}
@Transactional
override fun addLecture(courseId: Long, request: AddLectureRequest): LectureResponse {
val course = courseRepository.findByIdOrNull(courseId) ?: throw ModelNotFoundException("Course", courseId)
val lecture = Lecture(
title = request.title,
videoUrl = request.videoUrl,
course = course
)
// Course에 Lecture 추가
course.addLecture(lecture)
// Lecture에 영속성을 전파
courseRepository.save(course)
return lecture.toResponse()
}
override fun getLecture(courseId: Long, lectureId: Long): LectureResponse {
val lecture = lectureRepository.findByCourseIdAndId(courseId, lectureId)
?: throw ModelNotFoundException("Lecture", lectureId)
return lecture.toResponse()
}
override fun getLectureList(courseId: Long): List<LectureResponse> {
val course = courseRepository.findByIdOrNull(courseId) ?: throw ModelNotFoundException("Course", courseId)
return course.lectures.map { it.toResponse() }
}
@Transactional
override fun updateLecture(
courseId: Long,
lectureId: Long,
request: UpdateLectureRequest
): LectureResponse {
val lecture = lectureRepository.findByCourseIdAndId(courseId, lectureId)
?: throw ModelNotFoundException("Lecture", lectureId)
val (title, videoUrl) = request
lecture.title = title
lecture.videoUrl = videoUrl
return lectureRepository.save(lecture).toResponse()
}
@Transactional
override fun removeLecture(courseId: Long, lectureId: Long) {
val course = courseRepository.findByIdOrNull(courseId) ?: throw ModelNotFoundException("Course", courseId)
val lecture = lectureRepository.findByIdOrNull(lectureId)
?: throw ModelNotFoundException("Lecture", lectureId)
course.removeLecture(lecture)
// Lecture에 영속성을 전파
courseRepository.save(course)
}
@Transactional
override fun applyCourse(courseId: Long, request: ApplyCourseRequest): CourseApplicationResponse {
val course = courseRepository.findByIdOrNull(courseId) ?: throw ModelNotFoundException("Course", courseId)
val user = userRepository.findByIdOrNull(request.userId)
?: throw ModelNotFoundException("User", request.userId)
// Course 마감여부 체크
if (course.isClosed()) {
throw IllegalStateException("Course is already closed. courseId: $courseId")
}
// CourseApplication 중복 체크
if (courseApplicationRepository.existsByCourseIdAndUserId(courseId, request.userId)) {
throw IllegalStateException("Already applied. courseId: $courseId, userId: ${request.userId}")
}
val courseApplication = CourseApplication(
course = course,
user = user,
)
course.addCourseApplication(courseApplication)
// CourseApplication에 영속성을 전파
courseRepository.save(course)
return courseApplication.toResponse()
}
override fun getCourseApplication(courseId: Long, applicationId: Long): CourseApplicationResponse {
val application = courseApplicationRepository.findByCourseIdAndId(courseId, applicationId)
?: throw ModelNotFoundException("CourseApplication", applicationId)
return application.toResponse()
}
override fun getCourseApplicationList(courseId: Long): List<CourseApplicationResponse> {
val course = courseRepository.findByIdOrNull(courseId) ?: throw ModelNotFoundException("Course", courseId)
return course.courseApplications.map { it.toResponse() }
}
@Transactional
override fun updateCourseApplicationStatus(
courseId: Long,
applicationId: Long,
request: UpdateApplicationStatusRequest
): CourseApplicationResponse {
val course = courseRepository.findByIdOrNull(courseId) ?: throw ModelNotFoundException("Course", courseId)
val application = courseApplicationRepository.findByCourseIdAndId(courseId, applicationId)
?: throw ModelNotFoundException("CourseApplication", applicationId)
// 이미 승인 혹은 거절된 신청건인지 체크
if (application.isProceeded()) {
throw IllegalStateException("Application is already proceeded. applicationId: $applicationId")
}
// Course 마감여부 체크
if (course.isClosed()) {
throw IllegalStateException("Course is already closed. courseId: $courseId")
}
// 승인 / 거절 따른 처리
when (request.status) {
// 승인 일때
CourseApplicationStatus.ACCEPTED.name -> {
// 승인 처리
application.accept()
// Course의 신청 인원 늘려줌
course.addApplicant()
// 만약 신청 인원이 꽉 찬다면 마감 처리
if (course.isFull()) {
course.close()
}
courseRepository.save(course)
}
// 거절 일때
CourseApplicationStatus.REJECTED.name -> {
// 거절 처리
application.reject()
}
// 승인 거절이 아닌 다른 인자가 들어올 경우 에러 처리
else -> {
throw IllegalArgumentException("Invalid status: ${request.status}")
}
}
return courseApplicationRepository.save(application).toResponse()
}
}
테스트하는법
스웨거가서 포스트에 트라이 누르고 아무거나 입력
쿼리 확인하기
application.yml 의 spring 밑에
jpa:
properties:
hibernate:
show_sql: true
이런거 넣고 다시 스웹거에서 입력하면
Hibernate: insert into course (description,max_applicants,num_applicants,status,title,id) values (?,?,?,?,?,default)
이런게 나오면 된것
- 일대다 단방향
- 이제, 단방향은 @OneToMany 단방향을 할 것인가, @ManyToOne 단방향을 할 것 인가를 선택해야합니다. 먼저, @OneToMany 단방향 형태로 수정
- Course를 아래처럼 수정합니다. mappedBy 를 삭제하고, JPA에게 어떤 Column을 통해 관계가 성립되는지 알려주기 위해 @JoinColumn 을 붙여줍니다!
@Entity
@Table(name = "course")
class Course(
@Column(name = "title", nullable = false)
var title: String,
@Column(name = "description")
var description: String? = null,
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
var status: CourseStatus = CourseStatus.OPEN,
@Column(name = "max_applicants", nullable = false)
val maxApplicants: Int = 30,
@Column(name = "num_applicants", nullable = false)
var numApplicants: Int = 0,
@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY)
@JoinColumn(name = "course_id")
var lectures: MutableList<Lecture> = mutableListOf(),
@OneToMany(mappedBy = "course", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY)
var courseApplications: MutableList<CourseApplication> = mutableListOf()
)
- Lecture에서는 관계를 제거해 줍니다.
@Entity
@Table(name = "lecture")
class Lecture(
@Column(name = "title", nullable = false)
var title: String,
@Column(name = "video_url", nullable = false)
var videoUrl: String,
// @ManyToOne(fetch = FetchType.LAZY)
// @JoinColumn(name = "course_id")
// var course: Course
)
- 관련 코드도 수정해줘야겠죠? CourseServiceImpl 의 addLecture 메소드를 먼저 수정해주세요. Lecture를 생성할때 course를 명시할 필요가 없겠죠?
@Transactional
override fun addLecture(courseId: Long, addLectureRequest: AddLectureRequest): LectureResponse {
val course = courseRepository.findByIdOrNull(courseId) ?: throw ModelNotFoundException("Course", courseId)
val lecture = Lecture(
title = addLectureRequest.title,
videoUrl = addLectureRequest.videoUrl,
//course = course
)
// Course에 Lecture 추가
course.addLecture(lecture)
// Lecture에 영속성을 전파
courseRepository.save(course)
return lecture.toResponse()
}
- 그리고 돌리면!! 정상실행이 되지 않습니다. 왜 그럴까요? getLecture와 updateLecture에서 courseId와 lectureId 기반으로 lecture를 가져오는데, JPA는 이제 lecture가 courseId를 들고 있다는 걸 모르기 때문이죠! LectureRepository의 findByCourseIdAndId 를 주석처리 해주시고, findByCourseIdAndId 를 findByIdOrNull(lectureId) 로 변경해주세요.
package com.teamsparta.courseregistration.domain.lecture.repository
import com.teamsparta.courseregistration.domain.lecture.model.Lecture
import org.springframework.data.jpa.repository.JpaRepository
interface LectureRepository: JpaRepository<Lecture, Long> {
//fun findByCourseIdAndId(courseId: Long, lectureId: Long): Lecture?
}
-
override fun getLecture(courseId: Long, lectureId: Long): LectureResponse { val lecture = lectureRepository.findByIdOrNull(lectureId) ?: throw ModelNotFoundException("Lecture", lectureId) return lecture.toResponse() }
@Transactional override fun updateLecture( courseId: Long, lectureId: Long, updateLectureRequest: UpdateLectureRequest ): LectureResponse { val lecture = lectureRepository.findByIdOrNull(lectureId) ?: throw ModelNotFoundException("Lecture", lectureId) val (title, videoUrl) = updateLectureRequest lecture.title = title lecture.videoUrl = videoUrl return lectureRepository.save(lecture).toResponse() }
- 자, 이제 다시 lecture를 추가하면? 차이가 보이시나요? lecture에 title, video_url을 추가한 후, 이어서 update를 통해 course_id를 추가하는 걸 볼 수 있어요. 처음 insert될 때에는 course_id가 NULL로 설정이 됐다가 이후에 관계가 설정되는거죠!
select
l1_0.course_id,
l1_0.id,
l1_0.title,
l1_0.video_url
from
lecture l1_0
where
l1_0.course_id=?
insert
into
lecture
(title,video_url)
values
(?,?)
update
lecture
set
course_id=?
where
id=?
- 다대일 단방향
- 이제 반대로, 다대일 단방향, 즉 Lecture에서 Course의 연관관계를 갖게끔 설계를 해봅시다!
- 먼저, Lecture측에 Course를 다시 설정해줍니다.
package com.teamsparta.courseregistration.domain.lecture.model
import com.teamsparta.courseregistration.domain.course.model.Course
import com.teamsparta.courseregistration.domain.lecture.dto.LectureResponse
import jakarta.persistence.*
@Entity
@Table(name = "lecture")
class Lecture(
@Column(name = "title", nullable = false)
var title: String,
@Column(name = "video_url", nullable = false)
var videoUrl: String,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "course_id")
var course: Course
)
Course에서는 Lecture와의 관계를 제거해주시고, 관련 함수도 제거해주세요
package com.teamsparta.courseregistration.domain.course.model
import com.teamsparta.courseregistration.domain.course.dto.CourseResponse
import com.teamsparta.courseregistration.domain.courseapplication.model.CourseApplication
import jakarta.persistence.*
@Entity
@Table(name = "course")
class Course(
@Column(name = "title", nullable = false)
var title: String,
@Column(name = "description")
var description: String? = null,
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
var status: CourseStatus = CourseStatus.OPEN,
@Column(name = "max_applicants", nullable = false)
val maxApplicants: Int = 30,
@Column(name = "num_applicants", nullable = false)
var numApplicants: Int = 0,
// @OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY)
// @JoinColumn(name = "course_id")
// var lectures: MutableList<Lecture> = mutableListOf(),
@OneToMany(mappedBy = "course", cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.LAZY)
var courseApplications: MutableList<CourseApplication> = mutableListOf()
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
fun isFull(): Boolean {
return numApplicants >= maxApplicants
}
fun isClosed(): Boolean {
return status == CourseStatus.CLOSED
}
fun close() {
status = CourseStatus.CLOSED
}
fun addApplicant() {
numApplicants += 1
}
// fun addLecture(lecture: Lecture) {
// lectures.add(lecture)
// }
//
// fun removeLecture(lecture: Lecture) {
// lectures.remove(lecture)
// }
fun addCourseApplication(courseApplication: CourseApplication) {
courseApplications.add(courseApplication)
}
}
fun Course.toResponse(): CourseResponse {
return CourseResponse(
id = id!!,
title = title,
description = description,
status = status.name,
maxApplicants = maxApplicants,
numApplicants = numApplicants
)
}
- CourseServiceImpl 쪽에 에러가 많이 보일텐데요, 이를 수정해줍시다! 먼저, addLecture method에서는 이제 Course를 통한 영속성 전파가 아니라, lectureRepository를 이용한 저장을 해줘야합니다.
@Transactional
override fun addLecture(courseId: Long, addLectureRequest: AddLectureRequest): LectureResponse {
val course = courseRepository.findByIdOrNull(courseId) ?: throw ModelNotFoundException("Course", courseId)
val lecture = Lecture(
title = addLectureRequest.title,
videoUrl = addLectureRequest.videoUrl,
course = course
)
return lectureRepository.save(lecture).toResponse()
}
- getLectureList 에서는, 더 이상 course를 통한 lecture의 접근을 할 수 없어요. 그러므로, LectureRepository 에 coureId로 lecture 목록을 가져오는 인터페이스를 작성해서 사용해줘야합니다.
package com.teamsparta.courseregistration.domain.lecture.repository import com.teamsparta.courseregistration.domain.lecture.model.Lecture import org.springframework.data.jpa.repository.JpaRepository interface LectureRepository: JpaRepository<Lecture, Long> { //fun findByCourseIdAndId(courseId: Long, lectureId: Long): Lecture? fun findAllByCourseId(courseId: Long): List<Lecture> }
override fun getLectureList(courseId: Long): List<LectureResponse> {
return lectureRepository.findAllByCourseId(courseId).map { it.toResponse() }
}
package com.teamsparta.courseregistration.domain.lecture.repository
import com.teamsparta.courseregistration.domain.lecture.model.Lecture
import org.springframework.data.jpa.repository.JpaRepository
interface LectureRepository: JpaRepository<Lecture, Long> {
//fun findByCourseIdAndId(courseId: Long, lectureId: Long): Lecture?
fun findAllByCourseId(courseId: Long): List<Lecture>
}
- 이렇게 설정 후 재실행을 한 다음, Lecture 추가 API를 요청하면? 아래와 같이 select와 insert가 실행되는걸 볼 수 있어요!
select
c1_0.id,
c1_0.description,
c1_0.max_applicants,
c1_0.num_applicants,
c1_0.status,
c1_0.title
from
course c1_0
where
c1_0.id=?
insert
into
lecture
(course_id,title,video_url)
values
(?,?,?)
- 대다(OneToMany) 단방향 관계
- 장점
- 자식이 부모의 정보를 몰라도 된다면, 객체 지향적 관점에서 명확한 구조가 됩니다.
- 단점
- update쿼리가 추가적으로 발생됩니다.
- 외래키(Foreign Key)에 NOT NULL 과 같은 제약조건이 걸려있다면 실패합니다. (update 이전에 NULL 값으로 외래키를 insert 하므로)
- 개발시 주의점
- @JoinColumn 을 필수로 입력해줘야합니다.
- 장점
- 다대일(ManyToOne) 단방향 관계
- 장점
- 외래키의 관리 주체가 데이터베이스와 일치하여, 데이터 일관성이 높아집니다.
- 단점
- 역방향으로 연관된 Entity에 대한 참조가 없어, 역방향으로 접근하려면 별도의 쿼리가 필요합니다.
- 개발시 주의점
- @JoinColumn 을 필수로 입력해줘야합니다.
- 장점
- 양방향 관계
- 장점
- 두 Entity 간의 관계를 양측에서 접근할 수 있어, 쿼리 작성이 유연해집니다.
- 단점
- 양방향을 모두 신경써야 하므로 관리해야할 부분이 더 많습니다.
- 개발시 주의점
- 연관관계의 주인을 필수로 표시해줘야합니다. (mappedBy)
- 자식 Entity 저장시 부모 Entity까지 모두 입력해줘야합니다. (Kotlin을 사용하면서, nullable하지 않게 부모(e.g. Course)를 표기한다면, 컴파일러가 이런 부분을 알려줍니다. null-safe하지 않은 Java는 매우 주의해야하는 점이에요)
- 쿠키(Cookie)
- 이름, 값, 유효 시간, 도메인, 경로 정보가 포함됩니다.
- 서버에서 전송이 되어 클라이언트에 저장되고, 이후 같은 서버에서 요청이 있을 때마다 자동적으로 서버에 전송됩니다.
- 세션 (Session)
- 클라이언트에게 고유한 세션 ID를 부여하여 클라이언트 정보를 관리합니다. 이 때, 세션 ID를 부여하기 위해 일반적으로 쿠키를 사용합니다.
- 서버는 세션 ID 외의 클라이언트 정보를 서버 측 메모리나 별도 저장소를 이용하여 저장합니다.
이둘의 공통 단점은, 확장성이 좋지않습니다. 사용자가 많아지면, 보통 서버를 한 대가 아니라 여러 대를 두게 됩니다. 세션 정보는 보통 서버 애플리케이션 측 메모리에 저장하는데요, 이때 클라이언트가 기존에 요청하던 서버가 아닌 다른 서버에 요청하면 해당 서버는 클라이언트의 세션 ID가 없다고 판단하게 됩니다.
토큰(Token) 기반 인증
- 인증정보를 서버측에서 저장하고 있지 않고, 토큰이라는 데이터에 저장하여, 클라이언트가 토큰을 갖고 요청을 하게끔 하는 방식입니다.
단점은 보안상 취약
- egistered Claims
- 토큰 자체의 정보를 담은 Claim으로, 필수는 아니지만 Payload에 포함하기 권장되는 claim 들입니다. 아래 Claim들이 존재합니다.
- iss : Issuer, JWT의 발급 주체를 표기합니다. 예를들어, [sparta.com](<http://sparta.com>) 같은 형태가 될 수 있습니다!
- sub : Subject, JWT의 대상 주체, 즉 유저를 표기합니다. 유저의 ID등이 될 수 있겠죠!
- aud : Audience, 토큰을 받을 대상 그룹입니다. sub 와 헷갈릴수 있는데요,. 발급주체가 동일하더라도 해당 토큰이 어디서 쓰일지는 다를 수 있겠죠! 예를 들어, [auth.sparta.com](<http://auth.sparta.com>) 이라는 issuer에서 토큰을 발급하더라도, [course.sparta.com](<http://course.sparta.com>) 대상으로 발급을 할 수 도 있고, [admin.sparta.com](<http://admin.sparta.com>) 으로 발급을 할 수 도 있습니다. 보통 여러 시스템에 걸쳐 토큰을 사용할 수 있거나 한 시스템 내에서 토큰 별로 API 혹은 Resource에 대해 권한이 나뉘는, 큰 시스템에서 사용합니다.
- exp : Expiration time, JWT가 만료되는 시간입니다. timestamp 형태로 표현합니다.
- nbf : Not before time, 해당 시간 전에는 JWT가 사용되어서는 안됩니다. 잘 사용하지 않습니다. 예약작업 등에 사용할 수 있겠죠!
- iat : Issued at time, JWT가 발급된 시간입니다.
- jti : JWT ID, Uniquie Identifier 즉 식별자로, 매번 랜덤으로 해당 jti를 생성하여 동일한 정보 이더라도 생성할때마다 다른 JWT가 나오도록 하는 역할입니다. Replay Attack(보내는 메시지를 가로채 동일한 메시지를 다시 보내는 방식)을 방지하기 위해 쓰입니다.
- 일반적으로, iss, sub, exp 와 iat 둘중 하나를 사용하고, jti를 추가하기도 합니다. 나머지는 시스템 요구사항에 따라 선택적으로 사용합니다.
- 토큰 자체의 정보를 담은 Claim으로, 필수는 아니지만 Payload에 포함하기 권장되는 claim 들입니다. 아래 Claim들이 존재합니다.
- Custom Claims
- 필요에 의해 JWT에 추가적으로 담는 claim 입니다. email 정보, roles (권한) 정보등을 필요에 따라 추가할 수 있습니다. 비밀번호 같은 민감정보는 담기면 안돕니다.
- 서명(Signature) = 암호화
- 대칭키(Sercret Key, Symmetric Key) : 한 개의 키로 암호화 및 복호화가 가능할때, 이를 대칭키라 합니다.
- 비대칭키 (Asymmetric Key) : 공개키의 경우 개인키(Private Key)와 공개키(Public Key)로 구성됩니다. 대칭키와 다르게, 한 키로 서명을 하면, 다른 한키로 복호화를 할 수 있습니다. 이 특성을 이용해 두 가지를 할 수 있습니다.
- 공개키 암호화: 철수가 공개키를 다른 영희에게 주고, “나한테 메시지를 보낼때는 이 공개키로 암호화를 해서 보내!” 라고 한 후 개인키를 철수만이 알도록 보관을 한다면, 영희가 보내는 메시지(암호문)는 철수가 가진 키로만 복호화를 할 수 있으므로 철수만이 영희가 쓴 원본 메시지를 볼 수 있습니다.
- 서명: 철수가 메시지와 개인키로 해당 메시지를 암호화한 암호문을 영희에게 보내면서, “앞으로 메시지를 받을 때에는 공개키를 이용해서 내가 보낸 암호문을 복호화 하고, 내가 보낸 메시지와 정말로 일치하는지 확인해봐!” 라고 할 수 있습니다. 메시지가 서로 일치하면, 영희는 이 메시지가 철수가 보낸게 맞구나!를 확인할 수 있습니다.