๐ MVC ์ปจํธ๋กค๋ฌ ๋งคํ
Sprout Framework๋ ์์ฒญ ๋ผ์ฐํ , ํจ์ค ํจํด ๋งค์นญ, ํธ๋ค๋ฌ ๋ฉ์๋ ํด๊ฒฐ์ ์ฒ๋ฆฌํ๋ ์ ์ฐํ๊ณ ๊ฐ๋ ฅํ ์ปจํธ๋กค๋ฌ ๋งคํ ์์คํ ์ ์ ๊ณตํฉ๋๋ค. ์ด ์์คํ ์ ๋ค์ด์ค๋ HTTP ์์ฒญ์ ์ ์ ํ ์ปจํธ๋กค๋ฌ ๋ฉ์๋์ ๋งคํํ๊ธฐ ์ํด ํจ๊ป ์๋ํ๋ ์ฌ๋ฌ ํต์ฌ ๊ตฌ์ฑ ์์๋ฅผ ์ค์ฌ์ผ๋ก ๊ตฌ์ถ๋์์ต๋๋ค.
์ํคํ ์ฒ ๊ฐ์โ
MVC ์ปจํธ๋กค๋ฌ ๋งคํ ์์คํ ์ ์ฌ๋ฌ ํต์ฌ ๊ตฌ์ฑ ์์๋ก ๊ตฌ์ฑ๋ฉ๋๋ค:
- PathPattern: ๋ณ์, ์์ผ๋์นด๋, ์ ๊ท์์ ์ง์ํ๋ ๊ณ ๊ธ ํจํด ๋งค์นญ
- HandlerMethodScanner: ์ปจํธ๋กค๋ฌ ๋ฉ์๋๋ฅผ ๋ฐ๊ฒฌํ๊ณ ๋ฑ๋ก
- RequestMappingRegistry: ๋ชจ๋ ์์ฒญ ๋งคํ์ ์ค์ ๋ ์ง์คํธ๋ฆฌ
- HandlerMapping: ๋ค์ด์ค๋ ์์ฒญ์ ๋ํด ๊ฐ์ฅ ์ ํฉํ ํธ๋ค๋ฌ๋ฅผ ์ฐพ์
- HandlerMethodInvoker: ํด๊ฒฐ๋ ์ธ์๋ก ์ ํ๋ ํธ๋ค๋ฌ ๋ฉ์๋๋ฅผ ํธ์ถ
ํจ์ค ํจํด ๋งค์นญโ
PathPattern ํด๋์คโ
PathPattern
ํด๋์ค๋ ์ ๊ตํ URL ํจํด ๋งค์นญ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค:
public class PathPattern implements Comparable<PathPattern> {
// ํจํด: "/users/{id}/orders/{orderId}"
// ๋งค์น: "/users/123/orders/456"
public boolean matches(String path);
public Map<String, String> extractPathVariables(String path);
}
์ง์๋๋ ํจํด ๋ฌธ๋ฒโ
๊ฒฝ๋ก ๋ณ์โ
@GetMapping("/users/{id}")
public User getUser(@PathVariable String id) { }
@GetMapping("/users/{id}/orders/{orderId}")
public Order getOrder(@PathVariable String id, @PathVariable String orderId) { }
๊ฒฝ๋ก ๋ณ์์์ ์ปค์คํ ์ ๊ท์โ
// ์ปค์คํ
์ ๊ท์์ ๊ฐ์ง ํจํด
@GetMapping("/users/{id:\\d+}") // ์ซ์ ID๋ง ๋งค์น
public User getUser(@PathVariable String id) { }
์์ผ๋์นด๋โ
// ๋จ์ผ ์์ผ๋์นด๋ - ํ๋์ ๊ฒฝ๋ก ์ธ๊ทธ๋จผํธ ๋งค์น
@GetMapping("/files/*/download") // ๋งค์น: /files/image.jpg/download
// ์ด์ค ์์ผ๋์นด๋ - ์ฌ๋ฌ ๊ฒฝ๋ก ์ธ๊ทธ๋จผํธ ๋งค์น
@GetMapping("/static/**") // ๋งค์น: /static/css/main.css
๋จ์ผ ๋ฌธ์ ๋งค์นญโ
@GetMapping("/files/?.txt") // ๋งค์น: /files/a.txt, /files/1.txt
ํจํด ์ฐ์ ์์์ ๋ช ์๋โ
์ฌ๋ฌ ํจํด์ด ๋์ผํ ์์ฒญ์ ๋งค์น๋ ์ ์์ ๋ ํจํด์ ๋ช ์๋์ ๋ฐ๋ผ ์๋์ผ๋ก ์ ๋ ฌ๋ฉ๋๋ค:
@Override
public int compareTo(PathPattern other) {
// 1. ์ด์ค ์์ผ๋์นด๋(**)๊ฐ ์ ์์๋ก ๋ ๋ช
์์
int c = Integer.compare(this.doubleStarCount, other.doubleStarCount);
if (c != 0) return c;
// 2. ๋จ์ผ ์์ผ๋์นด๋(*)๊ฐ ์ ์์๋ก ๋ ๋ช
์์
c = Integer.compare(this.singleStarCount, other.singleStarCount);
if (c != 0) return c;
// 3. ๊ฒฝ๋ก ๋ณ์๊ฐ ์ ์์๋ก ๋ ๋ช
์์
c = Integer.compare(this.varNames.size(), other.varNames.size());
if (c != 0) return c;
// 4. ์ ์ ์ฝํ
์ธ ๊ฐ ๊ธธ์๋ก ๋ ๋ช
์์
c = Integer.compare(other.staticLen, this.staticLen);
if (c != 0) return c;
// 5. ์์ ์ ์ธ ์ ๋ ฌ์ ์ํ ์ฌ์ ์ ์์
return this.originalPattern.compareTo(other.originalPattern);
}
์ฐ์ ์์ ์์ ์์:
"/users/admin" // ๊ฐ์ฅ ๋ช
์์ (์ ์ ๊ฒฝ๋ก)
"/users/{id:\\d+}" // ๋ ๋ช
์์ (์ ์ฝ๋ ๋ณ์)
"/users/{id}" // ๋ ๋ช
์์ (์ ์ฝ๋์ง ์์ ๋ณ์)
"/users/*" // ๋ ๋ช
์์ (๋จ์ผ ์์ผ๋์นด๋)
"/**" // ๊ฐ์ฅ ๋ ๋ช
์์ (์ด์ค ์์ผ๋์นด๋)
์์ฒญ ๋งคํ ๋ฑ๋กโ
์ปจํธ๋กค๋ฌ ์ค์บโ
HandlerMethodScanner
๋ ์ปจํธ๋กค๋ฌ ํด๋์ค์ ํธ๋ค๋ฌ ๋ฉ์๋๋ฅผ ์๋์ผ๋ก ๋ฐ๊ฒฌํฉ๋๋ค:
@Component
public class HandlerMethodScanner {
public void scanControllers(BeanFactory context) {
for (Object bean : context.getAllBeans()) {
Class<?> beanClass = bean.getClass();
if (beanClass.isAnnotationPresent(Controller.class)) {
String classLevelBasePath = extractBasePath(beanClass);
for (Method method : beanClass.getMethods()) {
// ์์ฒญ ๋งคํ ์ด๋
ธํ
์ด์
์ ๋ํด ๊ฐ ๋ฉ์๋ ์ฒ๋ฆฌ
}
}
}
}
}
๊ฒฝ๋ก ๊ฒฐํฉโ
ํด๋์ค ๋ ๋ฒจ๊ณผ ๋ฉ์๋ ๋ ๋ฒจ ๊ฒฝ๋ก๊ฐ ์ง๋ฅ์ ์ผ๋ก ๊ฒฐํฉ๋ฉ๋๋ค:
@Controller
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}") // ๊ฒฐ๊ณผ: /api/users/{id}
public User getUser(@PathVariable String id) { }
@PostMapping("/") // ๊ฒฐ๊ณผ: /api/users/
public User createUser(@RequestBody User user) { }
}
์ด๋ ธํ ์ด์ ์ฒ๋ฆฌโ
์ค์บ๋๋ ๋ค์ํ ์์ฒญ ๋งคํ ์ด๋ ธํ ์ด์ ์ ์ง์ํฉ๋๋ค:
@RequestMapping(path = "/users", method = HttpMethod.GET)
@GetMapping("/users") // ๋์ผํ ์ถ์ฝํ
@PostMapping("/users")
@PutMapping("/users/{id}")
@DeleteMapping("/users/{id}")
@PatchMapping("/users/{id}")
์์ฒญ ๋งคํ ๋ ์ง์คํธ๋ฆฌโ
๋ฑ๋ก ๊ณผ์ โ
RequestMappingRegistry
๋ ๋ฐ๊ฒฌ๋ ๋ชจ๋ ๋งคํ์ ์ ์ฅํฉ๋๋ค:
@Component
public class RequestMappingRegistry {
private final Map<PathPattern, Map<HttpMethod, RequestMappingInfo>> mappings;
public void register(PathPattern pathPattern, HttpMethod httpMethod,
Object controller, Method handlerMethod) {
mappings.computeIfAbsent(pathPattern, k -> new EnumMap<>(HttpMethod.class))
.put(httpMethod, new RequestMappingInfo(pathPattern, httpMethod,
controller, handlerMethod));
}
}
ํธ๋ค๋ฌ ํด๊ฒฐโ
์์ฒญ์ด ๋ค์ด์ค๋ฉด ๋ ์ง์คํธ๋ฆฌ๋ ๊ฐ์ฅ ์ ํฉํ ํธ๋ค๋ฌ๋ฅผ ์ฐพ์ต๋๋ค:
public RequestMappingInfo getHandlerMethod(String path, HttpMethod httpMethod) {
List<RequestMappingInfo> matchingHandlers = new ArrayList<>();
// 1. ์์ฒญ ๊ฒฝ๋ก์ ๋งค์น๋๋ ๋ชจ๋ ํจํด ์ฐพ๊ธฐ
for (PathPattern registeredPattern : mappings.keySet()) {
if (registeredPattern.matches(path)) {
Map<HttpMethod, RequestMappingInfo> methodMappings = mappings.get(registeredPattern);
if (methodMappings != null && methodMappings.containsKey(httpMethod)) {
matchingHandlers.add(methodMappings.get(httpMethod));
}
}
}
if (matchingHandlers.isEmpty()) {
return null;
}
// 2. ํจํด ๋ช
์๋์ ๋ฐ๋ผ ์ ๋ ฌ (๊ฐ์ฅ ๋ช
์์ ์ธ ๊ฒ ์ฐ์ )
matchingHandlers.sort(Comparator.comparing(RequestMappingInfo::pattern));
// 3. ๊ฐ์ฅ ๋ช
์์ ์ธ ๋งค์น ๋ฐํ
return matchingHandlers.get(0);
}
ํธ๋ค๋ฌ ๋ฉ์๋ ํธ์ถโ
์ธ์ ํด๊ฒฐโ
HandlerMethodInvoker
๋ ๋ฉ์๋ ์ธ์๋ฅผ ํด๊ฒฐํ๊ณ ํธ๋ค๋ฌ๋ฅผ ํธ์ถํฉ๋๋ค:
@Component
public class HandlerMethodInvoker {
public Object invoke(RequestMappingInfo requestMappingInfo,
HttpRequest<?> request) throws Exception {
// ๋งค์น๋ ํจํด์์ ๊ฒฝ๋ก ๋ณ์ ์ถ์ถ
PathPattern pattern = requestMappingInfo.pattern();
Map<String, String> pathVariables = pattern.extractPathVariables(request.getPath());
// ๋ชจ๋ ๋ฉ์๋ ์ธ์ ํด๊ฒฐ
Object[] args = resolvers.resolveArguments(
requestMappingInfo.handlerMethod(), request, pathVariables);
// ํธ๋ค๋ฌ ๋ฉ์๋ ํธ์ถ
return requestMappingInfo.handlerMethod()
.invoke(requestMappingInfo.controller(), args);
}
}
์์ ํ ์์ โ
๋ชจ๋ ๊ตฌ์ฑ ์์๊ฐ ํจ๊ป ์๋ํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ฃผ๋ ํฌ๊ด์ ์ธ ์์ ์ ๋๋ค:
์ปจํธ๋กค๋ฌ ์ ์โ
@Controller
@RequestMapping("/api/v1")
public class BookController {
@GetMapping("/books")
public List<Book> getAllBooks() {
return bookService.findAll();
}
@GetMapping("/books/{id:\\d+}")
public Book getBook(@PathVariable Long id) {
return bookService.findById(id);
}
@GetMapping("/books/{category}/latest")
public List<Book> getLatestBooksByCategory(@PathVariable String category) {
return bookService.findLatestByCategory(category);
}
@PostMapping("/books")
public Book createBook(@RequestBody Book book) {
return bookService.save(book);
}
@PutMapping("/books/{id}")
public Book updateBook(@PathVariable Long id, @RequestBody Book book) {
return bookService.update(id, book);
}
@DeleteMapping("/books/{id}")
public void deleteBook(@PathVariable Long id) {
bookService.delete(id);
}
}
์์ฑ๋ ๋งคํโ
์ค์บ๋๋ ๊ฐ๊ฐ์ ํจํด๊ณผ ํจ๊ป ์ด๋ฌํ ๋งคํ๋ค์ ๋ฑ๋กํฉ๋๋ค:
HTTP ๋ฉ์๋ | ํจํด | ํธ๋ค๋ฌ ๋ฉ์๋ | ์ฐ์ ์์ |
---|---|---|---|
GET | /api/v1/books | getAllBooks() | ๋์ (์ ์ ) |
GET | /api/v1/books/{id:\\d+} | getBook() | ์ค๊ฐ (์ ์ฝ๋ ๋ณ์) |
GET | /api/v1/books/{category}/latest | getLatestBooksByCategory() | ์ค๊ฐ (์ ์ + ๋ณ์) |
POST | /api/v1/books | createBook() | ๋์ (์ ์ ) |
PUT | /api/v1/books/{id} | updateBook() | ๋ฎ์ (์ ์ฝ๋์ง ์์ ๋ณ์) |
DELETE | /api/v1/books/{id} | deleteBook() | ๋ฎ์ (์ ์ฝ๋์ง ์์ ๋ณ์) |
์์ฒญ ์ฒ๋ฆฌ ํ๋ฆโ
-
์์ฒญ ๋์ฐฉ:
GET /api/v1/books/123
-
ํจํด ๋งค์นญ:
/api/v1/books/{id:\\d+}
๋งค์น (์ซ์ ID์ ๋ํด ๊ฐ์ฅ ๋ช ์์ )/api/v1/books/{id}
๋ ๋งค์น๋์ง๋ง ๋ฎ์ ์ฐ์ ์์
-
ํธ๋ค๋ฌ ์ ํ:
getBook()
๋ฉ์๋ ์ ํ -
๊ฒฝ๋ก ๋ณ์ ์ถ์ถ:
{id: "123"}
-
์ธ์ ํด๊ฒฐ: "123"์
Long id = 123L
๋ก ๋ณํ -
๋ฉ์๋ ํธ์ถ:
bookController.getBook(123L)