2. Sever 개발의 멘탈모델
프론트엔드 개발자인 내가,,서버개발을 한다고 했을 때 뭐가 가장 다를까? 고민을 해보자면,
프론트엔드 개발은 1개의 빌드당 1개의 환경이 보장된다.
즉, 내가 작성한 코드는 사용자의 기기에서만 돌아가므로 각 환경에 대한 정책들을 고려하면 되는거고, 그걸 다 담아내면 되는 것. (폴리필이 가장 좋은 예시)
그런데 서버는 1개의 환경에서 구동된다. 그리고 여러 유저들이 딱 하나의 환경만 바라본다.
여기서부터 서버에서 신경써야할 것들이 파생된다.
- 동시성 (여러 요청 동시에 처리)
- 데이터 일관성 (DB 상태 관리)
- 보안 (인증,권한)
- 성능 (응답속도, 처리량)
- 안정성 (에러 처리, 모니터링)
1. Stateless
- 프론트엔드는 상태를 라이프사이클 동안 유지해야한다.
- 서버는 상태를 메모리에 저장하지 않는다. 그래서 stateless 함.
- 상태라는게 존재하지 않는다. 데이터베이스만 존재한다.
- 그러므로, 각 요청은 독립적이다.
2. 동시성
- 프론트엔드는 위에서도 말했듯 하나의 빌드파일이 하나의 환경에서만 동작함. 즉, 코드의 라이프사이클 내에서 한명의 사용자만 신경쓰면 된다.
- 하지만,서버는 하나의 환경에서 모든 요청을 받아야한다. 따라서 트랜잭션 같은게 필요하다.
3. 데이터중심 설계
- 프론트엔드는 결국 UI 중심으로 인터페이스가 설계된다.
- 서버는 데이터 중심이다. 이 데이터를 프론트엔드에서 필요한 방식대로 뿌려주는 형태가 API일 뿐이다. 즉, 서버가 진실의 원천이다.
4. Layer Architecture (계층구조)
- 컨트롤러 -> 서비스 -> 레포지토리 -> 엔티티
- 컨트롤러 (입구)
- 서비스 (비즈니스 로직)
- 레포지토리 (DB 접근)
- 엔티티 (실제 데이터)
// 전형적인 서버 구조
// 1. Controller (표현 계층)
@RestController
class ProductController(
private val productService: ProductService
) {
@PostMapping("/api/products")
fun create(@RequestBody request: CreateProductRequest): ProductResponse {
// 단순히 요청을 받아서 Service에 전달
val product = productService.create(request)
return ProductResponse.from(product)
}
}
// 2. Service (비즈니스 로직 계층)
@Service
@Transactional
class ProductService(
private val productRepository: ProductRepository,
private val userRepository: UserRepository
) {
fun create(request: CreateProductRequest): Product {
// 비즈니스 규칙 검증
val seller = userRepository.findById(request.sellerId)
?: throw UserNotFoundException()
if (request.price < 0) {
throw InvalidPriceException()
}
// 도메인 객체 생성 및 저장
val product = Product(
title = request.title,
price = request.price,
sellerId = seller.id
)
return productRepository.save(product)
}
}
// 3. Repository (데이터 접근 계층)
interface ProductRepository : JpaRepository<Product, Long> {
fun findBySellerId(sellerId: Long): List<Product>
fun findByPriceBetween(min: Int, max: Int): List<Product>
}
// 4. Entity (도메인 계층)
@Entity
data class Product(
@Id @GeneratedValue
val id: Long = 0,
val title: String,
val price: Int,
val sellerId: Long
)
5. 트랜잭션
@Service
@Transactional
class TradeService(
private val productRepository: ProductRepository,
private val orderRepository: OrderRepository,
private val userRepository: UserRepository,
private val notificationService: NotificationService
) {
fun completeTrade(productId: Long, buyerId: Long) {
// 1. 상품 상태 변경
val product = productRepository.findById(productId)
product.status = ProductStatus.SOLD
// 2. 주문 생성
val order = Order(buyerId, productId)
orderRepository.save(order)
// 3. 판매자 포인트 증가
val seller = userRepository.findById(product.sellerId)
seller.points += product.price
// 4. 알림 전송 (만약 여기서 에러 나면?)
notificationService.sendTradeComplete(buyerId, product)
// @Transactional이 있으면:
// → 모두 성공 or 모두 롤백
// → 중간에 실패하면 1,2,3도 취소됨
}
}
- All or Nothing (전부 성공 or 전부 취소)
- DB 의 일관성 보장
6. 보안
- FE 는 믿을수 없음.
- 모든 검증은 서버에서 하는 기조.
- 인증: 너 누구야
- 인가: 너 이거 할수있음?
// 프론트엔드 - 편의성 중심
function deleteProduct(id: number) {
// 사용자가 삭제 버튼 보이면 권한 있다고 가정
await api.delete(`/products/${id}`)
}
// 백엔드 - 항상 검증
@DeleteMapping("/api/products/{id}")
fun delete(@PathVariable id: Long, @AuthUser user: User) {
val product = productRepository.findById(id)
// 검증
if (product.sellerId != user.id) {
throw ForbiddenException("본인 상품만 삭제 가능")
}
productRepository.delete(product)
}