너구리소굴 백엔드 1차 개발 회고 (2) - Presentatiton/Persistence Layer 및 기타 초기 설정
백엔드 1차 개발 범위 중 저번 글에서 다루었던 인증, 비즈니스 로직(Application Layer)에 관한 내용은 제외하고 나머지 아래 항목에 대해 이번 글에 작성하려고 한다.
- Presentation Layer
- Persistence Layer
- 기타 초기 설정
Presentation Layer
1차 범위 내에서 Presentation Layer에 힘을 싣고 싶은 부분은 response body의 통일성이었다.
너구리 소굴 API 내에서는 ErrorResponseDto 형태로 ResponseBody를 제한하려고했다.
data class ErrorResponseDto(
val timestamp: String,
val status: Int,
val error: String,
val message: String?
) {
companion object {
fun of(status: HttpStatus, message: String?): ErrorResponseDto {
return ErrorResponseDto(
Instant.now().toString(),
status.value(),
status.reasonPhrase,
message
)
}
}
}
그리고 Companion object에서 of 메소드를 제공해서 ErrorResponseDto의 생성을 제한하고자 했다.
너구리소굴 API에서 에러가 발생하는 케이스는 크게 두 가지로 나눌 수 있다. 첫번째는 Authentication 과정(SecurityContext)에서 발생하는 예외들. 그리고 Handler를 실행하면서 발생하는 예외다.
Authentication 과정에서 발생한 예외 중 AuthenticationException의 자손 예외들만을 AuthenticationEntryPoint에서 처리할 수 있도록 설계되어있다. 너구리소굴 API에서는 NeoguriAuthenticationEntryPoint를 추가하여 이를 처리하도록 구현했다.
// NeoguriAuthenticationEntryPoint의 commence 메소드
override fun commence(request: HttpServletRequest?, response: HttpServletResponse?, authException: AuthenticationException?) {
logger.info("Exception caught while processing authentication, {}", authException!!.message)
val status = HttpStatus.UNAUTHORIZED
(response as HttpServletResponse)
response.status = status.value()
response.contentType = MediaType.APPLICATION_JSON_VALUE
objectMapper.writeValue(response.writer, ErrorResponseDto.of(status, authException.message))
}
Handler에서 발생하는 Exception들을 처리하기 위해서 NeoguriExceptionHandler를 구현했는데. @RestControllerAdvice, @ExceptionHandler 어노테이션을 이용해 exception handling을 설정해주었다.
@RestControllerAdvice
class NeoguriExceptionHandler {
/** ... */
@ExceptionHandler(HttpException::class)
fun handleHttpException(exception: HttpException): ResponseEntity<ErrorResponseDto> {
logger.info("status code: {}\nstackTrace: {}", exception.message, exception.stackTrace)
return ResponseEntity.status(exception.statusCode).body(
ErrorResponseDto.of(
exception.statusCode,
exception.message
)
)
}
}
Handler 영역에서 발생하는 PresentationLayer에서 발생 시키는 예외는 Response와 직접적으로 연관이 되어있기 때문에 모두 상태 코드를 포함하도록 HttpException이라는 타입을 새로 추가했고, 이를 확장하도록 구현했다.
abstract class HttpException(
val statusCode: HttpStatus,
override val message: String
) : RuntimeException(message) {}
Persistence Layer
영속성 처리를 관리하기 위한 로직을 구현한 클래스들을 Persistence Layer에 구현하겠다던 계획대로 Repository를 해당 레이어에 위치시켰다.
너구리소굴에서는 Spring JPA를 적용했기 때문에 Repository를 구현할 필요 없이 Interface를 이용해 비즈니스 로직을 구현해도 문제는 없었다. 하지만 너구리소굴에서는 Aggregate Root에서 하위 엔티티들의 수정, 관리에 대한 책임을 가지게 하고자 Aggregate Root 단위로 Repository를 별도로 구현할 예정이다.
@Repository
class UserEntityRepository(
val userRepository: UserRepositoryInterface,
val userFileRepository: UserFileRepositoryInterface,
val userAgreementRepository: UserAgreementRepositoryInterface,
val userNestRepository: UserNestRepositoryInterface
) : UserEntityRepositoryInterface {
override fun save(entity: User): User {
userRepository.save(entity)
if (entity.files !== null) {
userFileRepository.saveAll(CollectionConverter.mutableListToArrayList(entity.files!!))
}
if (entity.nests !== null) {
userNestRepository.saveAll(CollectionConverter.mutableListToArrayList(entity.nests!!))
}
if (entity.agreements !== null) {
userAgreementRepository.saveAll(CollectionConverter.mutableListToArrayList(entity.agreements!!))
}
return entity
}
/** ... */
}
예시로 UserEntityRepository 소스를 첨부하겠다. 위 소스에서 생성자를 통해 주입받는 의존성들은 모두 JpaRepository들이다. 위 save메소드처럼 entity에 특정 행위에 대한 setter를 통해 이미 값이 수정되어있는 entity를 인자로 받아 그 하위 엔티티들까지 한 번에 DB에 반영하는 처리를 수행하는데, 수정 행위를 수행하며 한 Entity와 그 부가정보에 대해 DB와 최대한 동기를 유지할 수 있으리란 기대로 이런 방법을 채택하였다.
개발 중 불편하다고 느끼는 점이 있다면 다시 회고하며 수정할 가능성은 있다.
기타 설정
마지막으로 Slf4j을 적용하였는데, 기본 설정에서 크게 바뀐 것은 없어서 언급할 내용은 없는 것 같다. 그리고 HATEOAS, Liquibase 등도 이번 개발 차수에 포함시키려 했지만 이번 1차 개발 범위에서는 아직 이들을 적용할 필요가 없다고 판단하여 2차 개발 범위에 포함시키려 한다. 이 둘 모두 아직 사용 경험이 거의 없다시피 한 도구들이어서 사용 전 충분한 리서치가 필요할 것 같다.
마무리
이 다음 작업으로는 너구리소굴 프론트 1차 개발에 대한 계획을 세우고 있는 중이다.
먼저 프레임워크 선정 등 개발 환경에 대한 리서치를 진행한 뒤 회원가입, 로그인 관련한 페이지를 추가하는 것까지를 프론트 1차 개발 범위로 설정했다.
백엔드 1차에 회원가입/로그인 기능이 이미 포함되어있지만 기능을 연결하지 않고 페이지 구현까지만 계획하고 있는 이유는 HATEOAS 때문이다. API 서버에서 어떤 자원을 제공해줄 때 HATEOAS를 적용한다면 그 자원에 대해 취할 수 있는 행위들에 대한 uri를 제공해줄텐데, 이걸 적용하지 않은 상태에서 API를 미리 연결하느니 적용한 뒤 작업하는 것이 더 공수가 적을 것이라고 생각했기 때문이다.
여기까지 대략 39일 정도 진행된 백엔드 1차 개발에 대한 회고를 마친다.