이 소설은 gemini-2.5-pro를 사용하여 작성되었습니다.
4화: 서비스와 레파지토리, 마법의 책임을 나누다
이안의 시연은 성공적이었다. 외부 세계와 통하는 문이 놀라울 정도로 빠르고 간결하게 만들어졌다. 하지만 로드 스프링의 날카로운 눈은 그 화려함 속에 숨겨진 구조적 문제를 정확히 짚어냈다.
"네가 만든 '컨트롤러'라는 문지기는 너무 많은 일을 하고 있다." 로드 스프링이 지적했다. "외부의 요청을 받는 동시에, 그 요청을 어떻게 처리할지에 대한 방법까지 알고 있구나. '치유 포션의 레시피를 알려줘'라는 요청에, 레시피를 찾는 방법(지금은 단순히 문자열을 조합하는 수준이지만)을 문지기가 직접 결정하고 있지. 만약 레시피를 찾는 방식이 복잡해진다면, 혹은 포션을 만드는 과정에 새로운 절차가 추가된다면, 문지기는 점점 비대해지고 둔해질 것이다. 이는 마법 설계의 기본 원칙, '단일 책임의 원칙'에 어긋난다."
그의 지적은 정확했다. 컨트롤러는 외부의 요청을 받아들이고 응답을 내보내는 역할에만 충실해야 했다. 실제 포션을 만들고 레시피를 찾는 등의 핵심적인 '비즈니스 로직'은 다른 존재가 담당해야 했다. 이는 스프링 가문이 오랫동안 지켜온 철학이기도 했다.
이안은 스승의 통찰력에 감탄하며 고개를 끄덕였다. "맞습니다, 스승님. 그래서 지금부터 마법의 역할을 분리하려고 합니다. 문지기(Controller), 장인(Service), 그리고 사서(Repository)로 말입니다."
이안은 새로운 개념을 설명하기 시작했다.
- 컨트롤러(@RestController): 오직 문지기의 역할만 수행한다. 외부의 요청을 받고, 그 요청을 처리할 수 있는 '장인'에게 전달한 뒤, 장인이 건네준 결과물을 다시 외부에 전달할 뿐, 작업의 구체적인 내용은 알지 못한다.
- 서비스(@Service): 마법의 핵심을 담당하는 장인이다. 컨트롤러로부터 요청을 받아, 실제 비즈니스 로직을 수행한다. 예를 들어 '포션 제조' 요청이 오면, '사서'에게 레시피를 요청하고, 재료의 유효성을 검사하며, 연금술 마법을 실행하는 등 일련의 과정을 조율하고 책임진다.
- 레파지토리(@Repository): 데이터베이스라는 거대한 도서관을 관리하는 사서다. 서비스가 요청하는 데이터를 찾아주거나, 새로운 데이터를 기록하고, 기존 데이터를 수정하거나 삭제하는 역할만을 수행한다. 데이터가 왜 필요한지, 그 데이터로 무엇을 할 것인지에 대해서는 관여하지 않는다.
설명을 마친 이안은 곧바로 코드 리팩토링에 들어갔다. 먼저 PotionService라는 클래스를 만들고 그 위에 @Service 룬을 새겼다. 그리고 PotionRepository라는 인터페이스를 만들고, 임시로 메모리에서 데이터를 관리하는 구현체 위에 @Repository 룬을 새겼다.
// PotionService.java
@Service
public class PotionService {
// ... 여기에 비즈니스 로직이 들어갈 예정 ...
public String findRecipe(String potionName) {
return potionName + "의 레시피는 마력의 샘물과 용의 비늘을 필요로 합니다.";
}
}
// PotionController.java
@RestController
public class PotionController {
// ??? 어떻게 PotionService를 사용하지?
}
이제 가장 중요한 순간이 남았다. 컨트롤러는 어떻게 서비스의 존재를 알고 그에게 일을 시킬 수 있을까? 전통적인 방식이라면, 컨트롤러가 직접 new PotionService() 와 같이 서비스 객체를 생성해야 했다. 하지만 이는 컨트롤러가 서비스의 구체적인 구현에 의존하게 되는, 유연하지 못한 구조를 만든다.
바로 이때, 스프링 가문의 핵심 철학, '의존성 주입(Dependency Injection)'이 다시 등장했다. 이안은 컨트롤러 클래스 내부에 단 한 줄의 코드를 추가했다.
// PotionController.java
@RestController
public class PotionController {
@Autowired
private PotionService potionService;
@GetMapping("/potions/{potionName}")
public String getPotionRecipe(@PathVariable String potionName) {
// 직접 로직을 처리하는 대신, 서비스에게 위임한다.
return potionService.findRecipe(potionName);
}
}
@Autowired.
이 룬을 본 로드 스프링의 눈이 커졌다. 그는 이 룬의 의미를 즉시 알아보았다. 컨트롤러는 '나는 PotionService 타입의 장인이 필요하다'고 선언했을 뿐, 그 장인을 어디서 어떻게 데려올지에 대해서는 전혀 신경 쓰지 않았다.
마법이 시작되면, @SpringBootApplication의 @ComponentScan 기능이 마법 세계를 샅샅이 뒤져 @Service 룬이 새겨진 PotionService 객체를 찾아낸다. 그리고 @Autowired 룬이 표시된 곳을 발견하면, 마치 약속이라도 한 듯, 미리 찾아두었던 PotionService 객체를 살며시 '주입'시켜 준다.
이것이 바로 '제어의 역전(Inversion of Control)'이었다. 객체를 생성하고 관리하는 권한(제어권)이 개발자(마법사)에게서 프레임워크(마법 세계)로 넘어간 것이다. 마법사는 그저 필요한 것을 선언하기만 하면, 마법 세계가 알아서 모든 것을 준비해 주었다.
이제 마법의 구조는 훨씬 더 명확하고 견고해졌다.
- 외부 요청이 컨트롤러의 문을 두드린다.
- 컨트롤러는
@Autowired로 주입받은 서비스에게 요청을 전달한다. - 서비스는 핵심 로직을 처리하고, 필요하다면 레파지토리에게 데이터를 요청한다.
- 서비스는 결과물을 컨트롤러에게 돌려준다.
- 컨트롤러는 받은 결과물을 외부 세계에 응답으로 내보낸다.
각자의 역할이 명확히 분리되어, 한 부분의 변경이 다른 부분에 미치는 영향을 최소화할 수 있었다. 이는 거대한 마법을 유지보수하는 데 있어 가장 중요한 원칙이었다.
로드 스프링은 이안이 재구성한 코드를 보며 깊은 생각에 잠겼다. 자신이 평생을 바쳐 XML 양피지에 그려왔던 복잡한 설계도가, 몇 개의 룬과 약속(관례)만으로 이토록 우아하게 구현될 수 있다는 사실. 그것은 늙은 아크메이지의 자존심에 상처를 입혔지만, 동시에 지적인 희열을 느끼게 했다.
이안의 '스프링 부트'는 단순히 빠른 마법이 아니었다. 그것은 스프링 가문의 핵심 철학을 가장 진보된 방식으로 구현해 낸, 정수(精髓) 그 자체였다.
'Spring & Boot > 소?설' 카테고리의 다른 글
| [두 개의 룬, 새로운 시대의 서막] 5화 (0) | 2025.09.08 |
|---|---|
| [두 개의 룬, 새로운 시대의 서막] 3화 (0) | 2025.09.08 |
| [두 개의 룬, 새로운 시대의 서막] 2화 (1) | 2025.09.08 |
| [두 개의 룬, 새로운 시대의 서막] 1화 (0) | 2025.09.08 |
댓글