연습용 주문 서버 만들기
가장 간단한 아이디 발급 API부터 만들겠습니다.
지난 번 글에도 말했듯이 편의상 회원가입없이 아이디 발급 요청하면 임의의 UUID를 발급하겠습니다.
이 아이디를 membership
이라고 하겠습니다. 사용자는 나중에 발급받은 membership
를 가지고 포인트를 충전하거나 결제할 수 있습니다.
구현할 요구사항은 아래와 같습니다.
- 사용자는 누구나
membership
발급 API를 통해 멤버십 번호를 발급받을 수 있다.
따로 무엇을 검증하지 않을 것이므로 매우 간단한 구현이 되겠습니다. 아이디 발급은 실패하는 케이스가 없다고 가정하겠습니다.
api 모듈 구현
api 모듈은 HTTP 요청을 처리하는 역할만 합니다. 실제 비즈니스 로직을 처리하는 부분은 core 모듈로 위임합니다.
order-api/src/main/com/order/api/MembershipPublishApiController 구현
@RestController
@RequestMapping("/api/v1")
class MembershipPublishApiController(
private val membershipService: MembershipService
) {
@PostMapping("membership")
fun publishMembership(): ResponseEntity<MembershipIdResponse> {
val membershipId = membershipService.publish()
return ResponseEntity(membershipId, HttpStatus.CREATED)
}
}
membershipService
에 멤버십 번호를publish
요청합니다.- 그 결과를
ResponseEntity
객체에 담아서 반환합니다.
아직은 MembershipService
를 만들지 않은 상태여서 컴파일 에러가 발생합니다.
core 모듈 구현
코어 모듈은 비즈니스 로직을 처리합니다. 여기서는 간단한 UUID를 발급하는 역할만 합니다.
order-core/src/main/com/order/core/membership/service/MembershipService
interface MembershipService {
fun publish(): MembershipIdResponse
}
- 모듈 간 통신은 인터페이스를 통해서만 합니다. 이렇게 하는 이유는 모듈 간 결합도를 낮추기 위해서입니다.
order-core/src/main/com/order/core/membership/service/code/MembershipDefaultService
@Service
internal class MembershipDefaultService(
private val publishService: MembershipPublishService
): MembershipService {
override fun publish(): MembershipIdResponse {
return publishService.createMembership()
}
}
~DefaultService
는 인터페이스에 대한 기본 구현입니다. 디폴트 서비스에서 실제 비즈니스 로직을 담당하는 객체에게 요청을 위임합니다.- 멤버십 발급을 담당하는
MembershipPublishService
을 주입받아서 실제로 멤버십을 생성하는 일을 위임합니다. 이렇게 하는 이유는MembershipServiceImpl
같은 클래스를 만들어서 모든 비즈니스 로직을 처리하는 거대한 클래스를 만들지 않기 위해서입니다. - 여기서 클래스를
internal
로 선언했습니다.internal
은 같은 모듈 안에서만 해당 클래스에 접근할 수 있다는 코틀린의 접근제어자입니다. 이렇게 접근 범위를 좁힌 이유는 다른 모듈에서 혹시라도 이 구체 클래스를 참조하는 일이 없게 만들기 위해서입니다. 모듈간 소통은 오직 public interface로만 하고, 그외 다른 구체 클래스에는 접근할 수 없습니다.
core/src/main/com/order/core/membership/service/code/MembershipPublishService
@Service
internal class MembershipPublishService(
private val membershipRepository: MembershipRepository
) {
fun createMembership(): MembershipIdResponse {
val newMembership = membershipRepository.save(Membership())
return MembershipIdResponse.of(newMembership)
}
}
--------
# DTO 객체
data class MembershipIdResponse(
val id: Long,
val membershipId: String
) {
companion object {
internal fun of(membership: Membership) = MembershipIdResponse(
id = membership.id,
membershipId = membership.membershipUUID
)
}
}
MembershipPublishService
는Membership
객체를 생성하여 DB에 저장한 뒤 DTO 객체를 반환합니다.- DTO 객체는
internal
이 아닌데 Controller에서도 반환용으로 사용하기 때문에 public합니다.
Repository & Membership
실제 데이터베이스에 접근하는 영역을 또다른 모듈로 분리할 수도 있을 것 같습니다. 하지만 규모가 작을 때는 하나의 모듈에서 처리하는 것이 좀더 경제적일 수도 있을 것 같다는 생각도 듭니다. 이번에는 구현 편의상 한 모듈에서
repository
internal interface MembershipRepository: JpaRepository<Membership, Long>
entity
@Entity
internal class Membership(
membershipUUID: String = UUID.randomUUID().toString()
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0;
val membershipUUID: String = membershipUUID
val createdAt: LocalDateTime = LocalDateTime.now()
}
아주 간단하게 이 정도로만 구현하겠습니다.
서버가 실행되는지 확인해보기 전에 몇가지 설정을 해주겠습니다.
order-api/src/main/resources/application.yml
spring.profiles.active: local
server:
port: 8080
spring:
config:
import:
- application-db.yml
main:
allow-bean-definition-overriding: true
order-core/src/main/resources/application-db.yml
spring:
jpa:
open-in-view: false
hibernate:
ddl-auto: none
properties:
hibernate:
default_batch_fetch_size: 100
---
spring.config.activate.on-profile: local
spring:
jpa:
hibernate:
ddl-auto: create-drop
properties:
hibernate:
show_sql: true
format_sql: true
database-platform: org.hibernate.dialect.MySQL8Dialect
database: mysql
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: 'root'
password: ''
url: jdbc:mysql://localhost:3306/order?useSSL=false&serverTimezone=Asia/Seoul
sql:
init:
mode: ALWAYS
logging:
level:
org.hibernate.type.descriptor.sql: trace
root/docker-compose.yml
version: '3'
services:
mysql:
container_name: order-db
image: mysql
environment:
MYSQL_DATABASE: "order"
MYSQL_ROOT_HOST: '%'
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
TZ: 'Asia/Seoul'
ports:
- '3306:3306'
command:
- 'mysqld'
- '--character-set-server=utf8mb4'
- '--collation-server=utf8mb4_unicode_ci'
restart: always
이 설정을 마치고 docker compose up
으로 도커를 실행합니다.
그리고 서버를 실행하면, 아래와 같이 테이블이 생성되며 서버가 실행됩니다.
간단한 테스트 코드로 정말로 멤버쉽 아이디를 발급해주는지 검증해보겠습니다.
@AutoConfigureMockMvc
@SpringBootTest
internal class MembershipPublishApiControllerTest {
@Autowired
lateinit var mockMvc: MockMvc
@Test
fun `membership 발급 요청 201 Created`() {
mockMvc.post("/api/v1/membership")
.andExpect {
status { isCreated() }
content {
contentType(MediaType.APPLICATION_JSON)
jsonPath("id") { exists() }
jsonPath("membershipId") { exists() }
jsonPath("membershipId") { isString() }
}
}.andDo {
print()
}
}
}
테스트를 실행하면 예상한대로 membershipId를 발급해주는 것을 알 수 있습니다.
'Spring' 카테고리의 다른 글
연습용 주문 서버 만들기 03 메뉴 목록 조회 API (0) | 2023.01.17 |
---|---|
연습용 주문 서버 만들기 01 Kotlin/Spring 멀티모듈 프로젝트 구성 (2) | 2022.12.22 |
Spring Scheduler 테스트 하기 (0) | 2022.11.28 |
Springboot 통합 테스트로 불안한 리팩토링에서 벗어나기 (2) | 2022.08.24 |
ConstraintValidator 그리고 유효성 검사에 대한 고민.. (0) | 2022.05.16 |