리포지토리에 이어 서비스와 컨트롤러 계층을 작성해보자
서비스
package simpleblog.service
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import simpleblog.domain.member.Member
import simpleblog.domain.member.MemberRepository
import simpleblog.domain.member.MemberRes
import simpleblog.domain.member.LoginDto
import simpleblog.domain.member.findMembersByPage
import simpleblog.domain.member.toEntity
import simpleblog.exception.MemberNotFoundException
@Service
@Transactional(readOnly = true)
class MemberService(
private val memberRepository: MemberRepository
) {
fun findAll(): List<MemberRes> = memberRepository.findAll().mapNotNull { it?.toDto() }
// it(member)이 null일 경우는 자동으로 제외
fun findAllByPage(pageable: Pageable): Page<Member?> = memberRepository.findMembersByPage(pageable)
@Transactional
fun saveMember(dto: LoginDto): Member {
return memberRepository.save(dto.toEntity())
}
fun deleteMember(id: Long) {
return memberRepository.deleteById(id)
}
fun findMemberById(id: Long): MemberRes {
return memberRepository.findById(id).orElseThrow {
MemberNotFoundException(id.toString())
}.toDto()
}
}
간단하게 기본적인 기능만 있는 서비스이다.
처음보는 어노테이션들과 코틀린 문법을 배워보자
- @Transactional
- 트랜잭션을 시작한다는 어노테이션이다. AOP 기반으로 작동하며, 따라서 public 메소드에만 작동한다.
- 이 어노테이션이 붙은 메소드가 실행되면, 프록시 객체가 요청을 가로채어서 트랜잭션 매니저에게 트랜잭션 시작을 요청하게 된다. 트랜잭션 매니저는 데이터소스에서 DB 커넥션을 가져오고, 자동 커밋을 비활성화한다. 자동 커밋이 비활성호되기 때문에 이 어노테이션이 붙은 메소드가 실패하면, 모든 과정은 롤백되게 된다.
이후 실제 객체에게 실행 흐름을 넘긴 다음, 모든 일이 성공하면 커밋하여 변경 사항을 DB에 기록한다. 커밋 또는 롤백이 완료되면 트랜잭션이 종료되고, 트랜잭션 매니저는 데이터소스에 DB 커넥션을 반납한다. - (readonly=true)
- 이 트랜잭션은 오직 읽기 로직으로만 사용하겠다는 의미이다. 그러면 JPA의 더티 체킹이 필요없어지므로 불필요한 스냅샷 비교나 연산을 건너뛸수 있게 된다. 이걸 걸어두면 성능이 향상된다는 곳도 있고, 아니라는 문서도 있는데 뭐가 정답일까??
- mapNotNull
- map 함수와 비슷한데, map을 수행한 결과가 null인 함수는 자동으로 제외해주는 함수이다. 자바에서는 스트림으로 map과 filter를 사용해서 비슷한 효과를 낼 수 있다.
val strings = listOf("1", "2", "abc", "4", "def")
val numbers = strings.mapNotNull { it.toIntOrNull() }
println(numbers) // 출력: [1, 2, 4]
위 코드와 비슷하게 PostService도 만들어주자
package simpleblog.service
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.security.access.annotation.Secured
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import simpleblog.domain.post.Post
import simpleblog.domain.post.PostRepository
import simpleblog.domain.post.PostRes
import simpleblog.domain.post.PostSaveReq
import simpleblog.domain.post.findPosts
import simpleblog.domain.post.toDto
import simpleblog.domain.post.toEntity
@Service
@Transactional(readOnly = true)
class PostService(
private val postRepository: PostRepository
) {
fun findPosts(pageable: Pageable): Page<Post?> {
return postRepository.findPosts(pageable)
}
fun savePost(dto: PostSaveReq): Post {
return postRepository.save(dto.toEntity())
}
fun deletePost(id: Long) {
return postRepository.deleteById(id)
}
fun findPostById(id: Long): PostRes {
return postRepository.findById(id).orElseThrow().toDto()
}
}
컨트롤러
package simpleblog.api
import org.springframework.data.domain.Pageable
import org.springframework.data.web.PageableDefault
import org.springframework.http.HttpStatus
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.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import simpleblog.domain.member.LoginDto
import simpleblog.service.MemberService
import simpleblog.util.value.CmResDto
@RestController
@RequestMapping("/member")
class MemberController(
private val memberService: MemberService
) {
@GetMapping("")
fun findAllByPage(
@PageableDefault(size = 20, sort = ["id"]) pageable: Pageable
): CmResDto<*> {
return CmResDto(HttpStatus.OK, "success", memberService.findAllByPage(pageable))
}
@GetMapping("/{id}")
fun findMemberById(@PathVariable id: Long): CmResDto<*> {
return CmResDto(HttpStatus.OK, "find member by id", memberService.findMemberById(id))
}
@DeleteMapping("/{id}")
fun deleteMember(@PathVariable id: Long): CmResDto<*> {
return CmResDto(
HttpStatus.OK, "delete member by id", memberService.deleteMember(id)
)
}
@PostMapping("")
fun saveMember(
@RequestBody dto: LoginDto
): CmResDto<*> {
return CmResDto(HttpStatus.OK, "save members", memberService.saveMember(dto))
}
}
멤버 관련 API를 다루는 컨트롤러다. MemberService를 주입받고 있다.
한가지 특별한 점은, 자바 스프링의 @RequestArgsConstructor처럼 final로 선언된 생성자를 만들어주는 어노테이션이 필요없이, 기본 생성자에 주입받을 빈만 선언해주면 된다는 것. 코틀린만의 편리 기능이다.
- @PageableDefault
- 이 어노테이션은 페이징을 위해 사용되는 Pageable 객체에서 기본 페이징 값을 설정하기 위해 사용한다.
Pageable 객체를 api에서 인자로 받겠다고 선언해두었을 경우, 호출할 때 url?size=10&page=0 과 같은 식으로 가져올 페이지와 페이지의 사이즈를 보낼 수 있다. 이 때, 인자를 설정해서 보내지 않았을 경우 서버에서 임의로 기본값을 설정할 수 있는 것이 PageableDefault 이다.
- 이 어노테이션은 페이징을 위해 사용되는 Pageable 객체에서 기본 페이징 값을 설정하기 위해 사용한다.
- @RestController
- @Controller + @ResponseBody를 합쳐놓은 편의 어노테이션이다.
스프링은 @Component가 붙은 클래스를 시동 시점에 스캔하여 빈으로 등록하는데, @Controller는 @Component와 포함하고 있어. 컴포넌트 스캔의 대상이 된다.
- @Controller + @ResponseBody를 합쳐놓은 편의 어노테이션이다.
- @RequestMapping
- 스프링에서 특정 URL 요청을 하면 어떤 자바 메소드가 처리할지 연결해주는 어노테이션이다. 파라미터에 method를 명시할 수 있는데, 이런 귀찮은 작업을 건너뛰고 @GetMapping, @PostMapping, @DeleteMapping 으로 사용할 수 있다.
반환 객체로는 새로운 객체 하나를 만들어서 사용했다.
class CmResDto<T>(
val resultCode:T,
val resultMsg:String,
val data:T
)
유사하게 다른 도메인의 컨트롤러도 구성해두었다.
package simpleblog.api
import jakarta.validation.Valid
import org.springframework.data.domain.Pageable
import org.springframework.data.web.PageableDefault
import org.springframework.http.HttpStatus
import org.springframework.security.access.annotation.Secured
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.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import simpleblog.domain.post.PostSaveReq
import simpleblog.service.PostService
import simpleblog.util.value.CmResDto
@RestController
@RequestMapping("/post")
class PostController(
private val postService: PostService
) {
@GetMapping()
fun findPosts(
@PageableDefault(size = 10, sort = ["id"]) pageable: Pageable
) : CmResDto<*> {
return CmResDto(HttpStatus.OK, "find Posts", postService.findPosts(pageable))
}
@GetMapping("/{id}")
fun findPostById(@PathVariable id: Long): CmResDto<*> {
return CmResDto(HttpStatus.OK, "find post by id", postService.findPostById(id))
}
@DeleteMapping("/{id}")
fun deletePost(@PathVariable id: Long): CmResDto<*> {
return CmResDto(
HttpStatus.OK, "delete post by id", postService.deletePost(id)
)
}
@PostMapping()
fun savePost(
@Valid @RequestBody dto: PostSaveReq
): CmResDto<*> {
return CmResDto(HttpStatus.OK, "find all posts", postService.savePost(dto))
}
}
다음은 스프링 시큐리티를 활용해서 액세스 토큰과 리프레시 토큰을 발급받는 로직을 작성해보자
'Spring > Kotlin' 카테고리의 다른 글
| Mockmvc로 통합 테스트 시 예외 감지 (0) | 2025.12.04 |
|---|---|
| 마이그레이션 계획 (0) | 2025.12.03 |
| 리포지토리 만들기 (0) | 2025.09.13 |
| 도메인 엔티티 작성 (0) | 2025.09.01 |
| AuditingEntity 작성 (2) | 2025.08.31 |