달력

7

« 2026/7 »

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
2026. 5. 24. 22:18

REST API의 Richardson Maturity Model Backend/Spring, ETC2026. 5. 24. 22:18

Intro

: 2008년 QCon(link)에서 Leonard Richardson은 개발자들이 만든 API가 REST를 준수하는지, 적합한지를 4단계로 분류하는 `성숙도 모델 - RMM(Richardson Maturity Model)`이라는 것을 발표했었습니다.

Level 0에서 Level 3까지 총 4단계로 분류되며, Level 3은 적용하기 상당히 불편하며 Over-Engineering으로 취급되는 편입니다. Level 2까지만 적용해도 충분히 좋은 REST API라고 취급되니 이정도를 목표로 삼으면 될 것 같아요.

 

Level 0

: URL이 행위 중심으로 이루어지는 단계라고 볼 수 있겠습니다. HTTP 위에 RPC를 얹은 것에 가까운데, 다음과 같은 느낌입니다.

/createUser
/deleteUser
/getUser
/updateUser

 

 HTTP Method를 그다지 활용하지 않으며, 행위가 API Model의 중심이 됩니다.

 

Level 1

: URL이 명사, resource 중심으로 이루어지는 단계입니다. 무엇(resource)을 다루고 있나?가 API Model의 중심이 되는데, 다음과 같은 느낌입니다.

/users
/users/1
/orders/1

 

 하지만 이 단계에서도 HTTP Method를 그다지 활용하지 않아요.

 

Level 2

: URL은 resource를 표현하고, HTTP Method로 action을 표현하는 단계입니다. 이걸 HTTP semantic을 활용한다고 표현하기도 해요. 다음과 같은 느낌입니다.

GET /users/1
POST /users
PUT /users/1
DELETE /users/1

 

Level 3 : HATEOAS(Hypermedia As The Engine Of Application State)

: 현재 state에서 client가 다음에 전이(transition)할 수 있는 state를 hypermedia link로 제공하는 단계입니다. API를 호출하면 resource에 할 수 있는 action들이 response에 포함되게 됩니다. 다음과 같은 response를 예시로 들 수 있겠습니다.

{
  "orderId": 1,
  "status": "PROCESSING",
  "_links": {
    "self": { "href": "/orders/123" },
    "cancel": { "href": "/orders/123/cancel", "method": "DELETE" },
    "payment": { "href": "/orders/123/payment", "method": "POST" }
  }
}

 

 response에서 _links를 보고 다음에 호출할 수 있는 action, 다음에 전이할 수 있는 state를 결정할 수 있습니다. orderId: 1에 해당하는 resource를 대상으로 할 수 있는 것들입니다. 적용하는게 상당히 불편하기도 해서 Level 3까지 적용하는 경우는 잘 없는데, 그래도 장단점은 존재합니다.

 

장점

  • Client-Server간 결합도를 낮출 수 있습니다.
  • API 구조 변경이 편합니다.
  • Dynamic하게 Workflow를 변경할 수 있습니다.

 

단점

  • Frontend와 협업이 까다로워질 수 있습니다
    : HATEOAS는 FSM(Finite State Machine) 기반의 상태 전이인데, 보통 Service의 UX는 Frontend가 주관하는게 일반적이에요. UX에 주는 영향까지 고려해서 _links 같은 것들을 결정하는 것은 어렵지 않을까요?
  • 개발 환경에서의 지원이 어려움
    : Swagger 같은 것들은 정적인 Endpoint를 기반으로 해서 동적인 Endpoint와는 잘 맞지 않습니다. 비슷한 이유로 Code Generation도 어려워요. 어떤 Endpoint를 사용할 수 있는지가 알기 어려우니까요.
  • Caching, CDN, prefetch 같은 것들도 전략을 세우기 어려움.
    : HATEOAS에서는 Client가 주도하는 최적화가 어렵습니다. 사용할 수 있는 Endpoint를 확정할 수가 없으니, prefetch 하려면 너무 많은 case를 고려해야 합니다. CDN도 같은 URL, Header인데 response가 달라질 수 있으므로 활용이 어렵겠죠. Caching도 마찬가지 입니다.

 

※ Hypermedia link?

: Level 3에서 다음에 전이할 수 있는 state, 할 수 있는 action을 hypermedia link로 제공한다고 처음에 이야기했었어요. 그냥 url, api path, endpoint라고 표현해도 될텐데 왜 Hypermedia link라고 표현했을까요?

 

 Hypermedia는 link를 통해 연결된 media를 뜻합니다. HTML은 link, action(같은 것으로...)을 담고있는 Hypermedia 문서라고 볼 수 있어요. Browser는 HTML 안에 있는 link, action 같은 것에 따라 다음 state로 전이할 수 있다고 볼 수 있습니다. Roy Fielding은 REST API에서도 Hypermedia를 통해 다음 상태로 전이할 수 있길 원했습니다. 그래서 Hypermedia link를 제공한다고 표현한 것이고... 또, 단순 url이라고 하기에는 "method": "DELETE"와 같이 method가 포함되어 있기도 하죠(api endpoint만으로 method를 추측할 수 있다면 method는 없어도 됩니다).

 

Reference

'Backend > Spring, ETC' 카테고리의 다른 글

kotlin.test.Test vs jupiter.api.Test  (0) 2025.10.25
:
Posted by syjdev

Intro

: MongoDB Collection에, Document들이 생성 후 10일이 지나면 제거되도록 TTL을 설정했었습니다.

db.MyCollection.createIndex(
  { createdAt: 1 },
  { expireAfterSeconds: 864000 }  // 10 * 86400, 10 Days
)

 

 그러던 어느 날, 설정한 TTL을 변경하려고 아래와 같이 명령어를 입력했는데요. System에 부하가 걸리면서 문제가 발생했습니다. 무엇이 문제였고, 그렇다면 어떻게 해야 했었을까요?

db.MyCollection.dropIndex("createdAt_1")
db.MyCollection.createIndex(
  { createdAt: 1 },
  { expireAfterSeconds: 432000 }  // 5 * 86400, 5 Days
)

 

많은 비용이 발생할 수 있는  createIndex

: dropIndex는 지우기만 하면 되므로 부하없이 순식간에 완료됩니다. 하지만 createIndex는 Collection에 존재하는 Document들을 전부 읽으면서 index를 생성하므로 Document가 많다면 부하가 크게 발생할 수 있습니다.

 과거에는 이러한 index 생성을 Foreground로도 실행할 수 있었는데, 다행히 4.4 Version 이후부터는 index 생성이 Background로만 동작하도록 개선되긴 했습니다. 그래도 여전히 I/O 부하가 커지는 문제는 발생할 수 있습니다.

 

 

collMod

1) collMod란?

: Collection의 option과 Index의 property를 설정하는 Command.

 

2) 변경 가능한 것들 (options)

2-1) Index Property

Property Description
expireAfterSeconds 만료 임계값을 정하는 시간(초).
hidden Query Planner에서 Index를 숨길지 여부.
prepareUnique Index에 중복을 허용할지 말지를 설정하는 것.
unique true인 경우, Index에 중복이 없다면 Unique Index로 전환함(false는 지원하지 않음).

 

2-2) Collection Options

Option Description
validator Collection에 대한 유효성 검사 또는 표현식
validationLevel update 중에, 기존 문서에 대해 유효성 검사를 어느정도로 엄격하게 적용할지에 대한 정도
validationAction 유효하지 않은 문서에 대한 처리 방법

 

이 외에도 View, Time-Series Collection를 수정하거나 Capped Collection의 크기를 조정할 떄에는 collMod를 쓸 수 있습니다.

 

3) 사용 방법

3-1) syntax

: collMod를 사용하기 위한 syntax는 다음과 같습니다.

 

db.runCommand(
   {
     collMod: <collection name or view name>,
     <option1>: <value1>,
     <option2>: <value2>,
     ...
   }
)

 

TTL 변경처럼, Index Property를 변경하려고 할 때에는 Option에 명시해야 하는 것이 조금 복잡한데요. 다음과 같습니다.

 

db.runCommand( 
   {
     collMod: <collection>,
     index: {
        keyPattern: <index_spec> | name: <index_name>,
        expireAfterSeconds: <number>,  // Set the TTL expiration threshold
        hidden: <boolean>,             // Change index visibility in the query planner
        prepareUnique: <boolean>,      // Reject new duplicate index entries
        unique: <boolean>              // Convert an index to a unique index
     },
     dryRun: <boolean>                 // default: false, Only for index.unique is true.
   } 
)

 

3-2) example

3-2-1) TTL 변경하기

db.runCommand({
  collMod: "MyCollection",
  index: {
    keyPattern: { createdAt: 1 },
    expireAfterSeconds: 432000
  }
})

 

3-2-2) validator 변경하기

db.runCommand({
  collMod: "MyCollection",
  validator: {
    age: { $gte: 0 }
  }
})

 

 

4) collMod 사용시 주의사항

4-1) collMod는 replication됨 => oplog entry 생성.

: oplog entry 생성의 의미는 다음과 같습니다.

 

primary에서 실행된 command를 replica set에서도 실행되도록
operation log에 기록하는 것

 

 따라서 replica set이 있는 환경이라면, collMod로 무거운 작업을 수행히 Secondary에서도 동일하게 무거운 작업을 수행하게 됩니다. 이로인해 발생할 수 있는 문제점들로 다음과 같은 것들이 있습니다.

  • Replication Lag 증가
  • Disk I/O 증가
  • oplog Overflow
    : oplog의 size가 작다면, Secondary가 처리하지 못했는데 oplog entry가 삭제될 수 있습니다. 이런 문제가 생긴 상태를 oplog gap이라고 하고, 이때부터는 Secondary는 더 이상 Incremental Replication을 할 수 없습니다.
    • MongoDB는 oplog를 감지하고 자동으로 initial sync를 하게 되는데, 이건 심각항 상황을 초래할 수 있어요.

 

※ initial sync

: replica set에서 secondary가 primary의 oplog를 따라잡지 못하면, MongoDB가 수행하는 작업입니다. 다음과 같은 과정을 수행하게 되는데...

 

1. primary의 Collection들 복사
2. index 복사
3. oplog replay

 

그냥 DB 전체를 copy한다고 보시면 되겠습니다...

 

 initial sync는 매우 오래 걸리는 작업이고 그동안 해당 Secondary는 replication에 참여할 수 없습니다(Fault Tolerance가 낮아지겠네요). Primary에 장애가 발생하고, failover에 참여할 수 있는 Secondary가 없다면 큰일이겠네요. 또, Primary에서 Disk I/O, Network I/O가 높아지게 될 겁니다.

 

※ TTL을 갑자기 크게 줄이지 말 것
: collMod로 TTL을 줄이는 것 자체는 굉장히 짧게 끝나는 연산이지만, TTL을 변경함으로 인해 삭제해야 하는 Document가 많아진다면 initial sync가 발생할 수 있습니다. 어쩌면 여러 Secondary가 동시에 initial sync를 할지도 모릅니다.

 

4-2) collMod는 해당 collection에 lock을 건다.

: collMod는 보통 가벼운 작업입니다. Index의 TTL을 변경하는 정도의 작업에선 작업 시간이 ms 단위여서 lock이 걸려도 크게 문제될 일이 없습니다. 하지만, 크기가 매우 큰 Collection에서 다음과 같은 작업을 하면 문제가 될 수 있습니다.

 

collMod로 prepareUnique를 true로 변경
그 다음, collMod로 unique를 true로 변경

 

 위와 같은 작업을 하면 index 중복 검사를 하게 되는데, 이때 Collection Scan이나 Index Scan이 발생할 수 있습니다. 그러면 생각보다 lock이 오래 지속될 수 있습니다.

 

Reference

:
Posted by syjdev
2025. 10. 25. 23:44

kotlin.test.Test vs jupiter.api.Test Backend/Spring, ETC2025. 10. 25. 23:44

Intro

: 어느 날 Unit Test를 작성하던 중에, 사용하고 있던 Test Annotation에 궁금증이 생겼습니다. kotlin.test.Testorg.junit.jupiter.api.Test 모두 @Test Annotation을 제공하는데 각각 어떤 차이가 있는 것일까요? 그리고 둘중 하나를 선택해서 쓰는 이유는 무엇일까요? 내가 사용하는 기술 셋에 대해, 사용 목적조차 모르고 있었다는 것에 반성하며 정리해 보았습니다.

 

kotlin.test.Test

: kotlin.test.Test는 kotlin의 표준 Test annotation이고, 언어 차원에서 제공하며 특정 Framework/Platform에 의존적이지 않은 Test Annotation입니다(kotlin 표준 library에 포함돼 있음 - kotlin.test package).

 

이 annotation이 붙은 test 실행은 Platform에 따라 차이가 있어요.

  • JVM에서는 JUnit으로 Mapping되어 실행됨.
  • Kotlin Native에서는 kotlin/native test runner로 Mapping되어 실행됨.

 

저의 경우는 JVM에서 돌아가는 app을 개발하고 있었으니, kotlin.test.Test annotation을 붙이더라도 JUnit test와 별반 차이가 없었습니다. 다만 JUnit의 기능인 @BeforeEach 같은 것은 지원하지 않는다고 하네요.

 

org.junit.jupiter.api.Test

: JUnit에서 제공하는 Test annotation입니다. annotation 자체에는 별 속성같은 것도 없지만, JUnit에서 annotation을 인식하여 test를 실행합니다.

 

※ kotlin.test.Test annotation과 JUnit의 기능을 같이 쓰면...?

: 만약에 @Test annotation에 대한 의존성을 잘못 추가하여, kotlin.test.Test annotation과 JUnit의 기능을 함께 사용하도록 test code를 작성하면 어떻게 될까요(예를들면 아래와 같은 code)?

import org.junit.jupiter.api.BeforeEach
import kotlin.test.Test

class AddUseCaseTestMultipleCase {
    lateinit var addUseCase: AddUseCase

    @BeforeEach
    fun setUp() {
        addUseCase = AddUseCase()
    }

    @Test
    fun 1 + 2 = 3() {
        val result = addUseCase.add(1, 2)
        assertEquals(result, 3)
    }

    @Test
    fun -1 + 2 = 1() {
        val result = addUseCase.add(-1, 2)
        assertEquals(result, 1)
    }
}

 

다행히도 문제없이 @BeforeEach annotation이 개별 test가 실행되기 전에 실행됩니다.

 

kotlin.test.Test는 어떤 Framework/Platform에서도 실행될 수 있도록, 다른 Test framework로 Mapping됩니다(JVM 환경이라면, kotlin.test.Test -> org.junit.jupiter.api.Test로 Mapping). 그래서 각 @Test는 JUnit의 Test annotation과 동일하게 처리되고, @BeforeEach도 개별 test가 실행되기 전에 호출됩니다.

 

Conclusion

: 당장은 kotlin.test.Test annotation을 사용하더라도 문제가 되지 않을 수 있지만, 지금 문제없이 동작한다고 다가 아닌 것 같아요. 제가 의도한대로 code를 작성했느냐가 중요하다고 생각합니다. 그런 관점에서 kotlin.test.Test를 사용하는 것은 제가 의도한 바가 아니고, 그걸 확인하는 과정에서 JUnit의 Test Annotation과 비교해본 것은 의미가 있었던 것 같습니다.

'Backend > Spring, ETC' 카테고리의 다른 글

REST API의 Richardson Maturity Model  (0) 2026.05.24
:
Posted by syjdev