본문으로 건너뛰기
2026. 7. 12·약 13분

스프링(Spring)은 프론트엔드의 무엇과 닮았을까요?

카드 석 장이 뒤로 물러나며 겹치고 맨 앞 흰 카드에 스프링 잎이 있는 그림

백엔드 레포에서 @RestController가 붙은 코틀린 파일을 처음 열었을 때, 코드 자체는 이상하리만치 익숙한데 코드를 감싼 용어들이 낯설었습니다. 의존성 주입(DI), 제어의 역전(IoC), 관점 지향(AOP), 컨테이너, 빈(Bean). 필기 노트를 아무리 다시 읽어도 자바·백엔드 진영의 단어로만 설명되어 있어 좀처럼 손에 잡히지 않았습니다.

하나씩 읽다 보니 익숙한 부분이 보였습니다. 인터페이스로 결합도를 낮추고, 공통 처리를 한곳에 모으고, 반복되는 설정을 자동화하는 일은 프론트엔드에서도 해봤습니다. 그 경험을 떠올리니 낯선 코드가 조금씩 읽히기 시작했습니다.

스프링의 개념들은 프론트엔드의 무엇에 대응하고, 왜 하필 그렇게 생겼을까요?


이 글에서 다루는 내용​

스프링(Spring)을 처음 접한 프론트엔드 개발자가 백엔드 코드를 읽는 데 필요한 개념을 정리합니다. 객체를 만들고 연결하는 방식부터 HTTP 요청 처리와 DB 접근, 설정과 테스트까지 따라갑니다. React·TypeScript 등 익숙한 도구와 비교하되, 스프링이 실제로 어떤 일을 대신하는지에 초점을 둡니다. 자바나 코틀린 문법은 예제에서 필요한 만큼 설명합니다.





스프링은 무엇을 하는 프레임워크인가​

스프링은 자바·코틀린 진영에서 사실상 표준으로 쓰이는 애플리케이션 프레임워크입니다. 흔히 웹 프레임워크로 알려져 있지만, 그 핵심은 뒤에서 다룰 IoC 컨테이너이고 웹 기능(Spring MVC)은 그 컨테이너를 바탕으로 만든 기능 가운데 하나입니다. 기능 목록으로 소개할 수도 있지만, 그보다 먼저 이 프레임워크가 지향하는 목표를 이해하는 편이 훨씬 도움이 됩니다.

코드를 느슨하게 결합(loose coupling)하여, 변화에 유연하고 테스트하기 쉬운 구조를 만든다.

스프링이 제공하는 거의 모든 장치는 이 한 문장을 실현하기 위한 도구입니다. 프론트엔드 개발자에게 이 목표는 전혀 새롭지 않습니다. React에서 화면을 레고 블록 같은 컴포넌트로 잘게 쪼개고, 각 컴포넌트가 서로의 내부를 모른 채 props라는 계약으로만 대화하게 만드는 이유가 바로 이것이기 때문입니다.

스프링은 같은 발상을 백엔드의 비즈니스 로직에 적용합니다. 결제, 정산, 알림 같은 핵심 로직을 독립된 블록으로 만들어 조립하고, 블록끼리 강하게 얽매이지 않게 합니다. 이렇게 느슨하게 결합해두면 다음과 같은 이점이 생깁니다.

  • 재사용성: 특정 기능 블록만 떼어 다른 곳에서 다시 쓰기 쉬워집니다.
  • 테스트 용이성: 검증하려는 블록만 떼어내 가짜 객체(mock)로 주변을 채우고 단위 테스트를 돌릴 수 있습니다.



빈(Bean)과 컨테이너, 그리고 어노테이션​

스프링을 이해하는 가장 좋은 출발점은 빈(Bean)과 컨테이너(container)입니다. 앞으로 나올 거의 모든 개념이 이 둘을 전제로 하기 때문입니다.


빈은 스프링이 대신 관리하는 객체입니다​

빈(Bean)은 스프링 컨테이너가 관리하는 객체입니다. @Bean 메서드 안에서 직접 생성한 객체도 컨테이너에 등록하면 빈이 됩니다. 그리고 그 빈들을 담아두고, 필요할 때 꺼내주고, 서로 연결해주는 커다란 상자가 컨테이너(스프링에서는 ApplicationContext라고 부릅니다)입니다.

그러면 어떤 객체가 빈이 될까요? 클래스 위에 어노테이션(annotation)을 붙여 "이건 스프링이 관리해줘"라고 표시합니다. 표시의 종류는 역할에 따라 나뉩니다.

어노테이션붙이는 대상
@Component특별한 역할이 없는 일반적인 빈
@Service비즈니스 로직을 담는 빈
@RepositoryDB 접근을 담당하는 빈
@Controller · @RestController웹 요청을 받는 빈

@Service, @Repository, @Controller는 사실 @Component의 특수화된 형태입니다. 기능은 거의 같고, 이름으로 "이 클래스가 어떤 층에 속하는지"를 드러내는 역할이 큽니다. 스프링은 애플리케이션이 시작될 때 패키지를 훑어(컴포넌트 스캔, component scan) 이 어노테이션이 붙은 클래스를 모두 찾아 빈으로 등록합니다.


어노테이션은 데코레이터입니다​

스프링 코드의 @Service 같은 표시는 프론트엔드의 데코레이터(decorator)를 떠올리면 읽기 쉽습니다. 클래스나 메서드에 역할을 표시한다는 점이 닮았습니다. 여기서는 스프링이 그 표시를 읽고 객체 등록 같은 처리를 수행한다는 점을 기억하면 됩니다.

Angular의 @Injectable()·@Component(), NestJS의 @Controller()·@Get()을 써봤다면 이런 표기법이 익숙할 것입니다. 다만 각 표시가 실제로 무엇을 하는지는 프레임워크별로 확인해야 합니다.


기본 스코프는 싱글턴입니다​

컨테이너에 등록된 빈은 기본적으로 싱글턴(singleton) 으로 관리됩니다. 같은 컨테이너에서 같은 빈 정의에 대해 인스턴스 하나를 공유한다는 뜻입니다. 같은 클래스라도 빈 정의나 컨테이너가 다르면 인스턴스가 여럿일 수 있습니다. 요청이 올 때마다 서비스 객체를 새로 만드는 게 아니라, 앱이 뜰 때 만들어 둔 인스턴스를 계속 재사용합니다.

프론트엔드로 옮기면, 모듈 스코프에 인스턴스를 하나 만들어 앱 전체가 import해 쓰는 것, 예컨대 apiClient 하나를 만들어 모든 곳에서 공유하는 패턴과 같습니다.


Recap​

빈(Bean)은 스프링 컨테이너(ApplicationContext)가 관리하는 객체입니다. 컴포넌트 스캔은 @Component·@Service 같은 어노테이션이 붙은 클래스를 찾아 빈으로 등록합니다. 기본 스코프는 싱글턴이라 같은 컨테이너 안에서 해당 빈의 인스턴스를 공유합니다.




의존성 주입(DI)과 제어의 역전(IoC)​

컨테이너가 빈들을 관리한다면, 그 빈들을 서로 연결하는 방식이 의존성 주입(DI, Dependency Injection)과 제어의 역전(IoC, Inversion of Control)입니다. 이름은 둘이지만 서로 떼어 설명하기 어려운 개념입니다.


문제는 new에서 시작합니다​

어떤 컴포넌트가 다른 객체를 필요로 할 때, 그 객체를 내부에서 직접 생성하면 어떤 일이 벌어지는지부터 봅니다.

// 컨트롤러가 필요한 구현체를 내부에서 직접 만들어 끌어안는다
@RestController
class NoDiController {
// 구체적인 구현체(MyServiceImpl)에 직접 의존한다: 결합도가 극도로 높다
private val service: MyService = MyServiceImpl()

@GetMapping("/hello")
fun getHello(): String = service.getHello()
}

이 코드는 MyServiceImpl이라는 구체적인 구현체를 컨트롤러가 직접 new로 만들어 쥐고 있습니다. 이 구조에는 다음 문제가 있습니다.

  • 교체가 어렵습니다. 나중에 MyServiceImpl을 AdvancedServiceImpl로 바꾸려면 컨트롤러 코드 자체를 열어 고쳐야 합니다.
  • 의존성을 교체하는 테스트가 어렵습니다. 단위 테스트에서 service를 가짜 객체로 갈아끼울 방법이 없습니다. 진짜 DB나 네트워크를 타는 구현체가 컨트롤러 안에 못 박혀 있기 때문입니다.

프론트엔드로 옮기면, 컴포넌트가 자기 안에서 new ApiClient()를 직접 만들어 쓰는 것과 같습니다. 그 컴포넌트를 테스트하려고 하면 진짜 네트워크 요청이 나가 버려 곤란했던 경험과 같은 문제입니다.


주입받으면 달라집니다​

DI는 이 관계를 뒤집습니다. 컴포넌트는 "나는 이런 인터페이스가 필요하다"고 선언만 하고, 실제 알맹이는 바깥에서 넣어(inject) 줍니다.

// 1. 알맹이(구현체)는 스프링이 관리하도록 등록만 해둔다 (앞서 본 빈 등록)
@Service
class MyServiceImpl : MyService {
override fun getHello(): String = "Hello"
}

// 2. 컨트롤러는 인터페이스만 바라보고, 생성자로 주입받는다
@RestController
class DiController(
private val myService: MyService // 스프링이 알아서 MyServiceImpl을 넣어준다
) {
@GetMapping("/hello")
fun getHello(): String = myService.getHello()
}

이제 컨트롤러는 MyService라는 인터페이스만 알 뿐, 그 뒤의 구현체는 모릅니다. 인터페이스 계약이 유지되는 한 구현체를 바꿔도 컨트롤러는 수정하지 않아도 됩니다. 테스트에서는 MockMyService를 주입할 수 있습니다.

프론트엔드에서 props로 모킹된 함수를 넘겨 컴포넌트를 테스트하는 것과 같은 이치입니다. 컴포넌트가 콜백을 직접 만들지 않고 부모에게서 받아 쓰듯, 컨트롤러도 의존 객체를 스스로 만들지 않고 바깥에서 받습니다.


"역전"된 것은 무엇인가​

여기서 IoC의 "역전"이 무엇을 뒤집었는지가 드러납니다. 원래는 내가 필요한 객체를 언제 어떻게 만들지 통제했습니다(new MyServiceImpl()). DI에서는 그 통제권이 스프링 컨테이너(container)로 넘어갑니다. 객체를 만들고, 서로 연결하고, 수명을 관리하는 통제권을 이렇게 프레임워크로 넘기는 것이 제어의 역전입니다. DI는 그 역전을 구현하는 구체적인 방법입니다.

React에서도 <MyComponent />를 선언하면 런타임이 컴포넌트의 생명주기를 관리합니다. 실행과 수명 관리를 프레임워크에 맡긴다는 점에서 IoC를 이해하는 데 도움이 되는 비유입니다.


Recap​

의존성 주입(DI)은 필요한 객체를 바깥에서 전달받는 방식입니다. 위 예제처럼 인터페이스에 의존하면 테스트에서 가짜 구현을 넣기 쉽습니다. 스프링 컨테이너가 객체의 생성·연결·수명을 관리하는 것은 제어의 역전(IoC)의 한 예이고, DI는 이를 구현하는 방법입니다.




관점 지향 프로그래밍(AOP)​

로깅, 권한 검사, 트랜잭션 관리(DB 작업의 성공·실패를 묶어 처리) 같은 기능은 어떤 API를 만들든 공통으로 따라붙습니다. 이런 부가 기능을 비즈니스 로직 한복판에 일일이 끼워 넣으면, 정작 중요한 로직(예: 급여 정산 계산)이 로그 코드와 권한 코드에 파묻혀 지저분해집니다. 이렇게 여러 곳에 흩어지는 공통 관심사를 횡단 관심사(cross-cutting concerns)라 부르고, 이것을 한곳에 모아 모듈화하는 기법이 관점 지향 프로그래밍(AOP, Aspect-Oriented Programming)입니다.


이미 쓰고 있는 개념입니다​

Axios 인터셉터(interceptor)나 고차 컴포넌트(HOC, Higher-Order Component)에서 공통 처리를 모아본 경험이 있다면 AOP의 목적을 이해하기 쉽습니다.

Axios에서는 요청에 토큰을 붙이거나 응답의 에러를 처리하는 코드를 인터셉터 한곳에 모읍니다. AOP도 여러 곳에서 필요한 처리를 모아 각 업무 코드에 반복해서 적는 일을 줄입니다.

  • 핵심 관점: 진짜 비즈니스 로직 (예: 급여를 정산한다)
  • 부가 관점: 모든 API에 공통으로 필요한 기능 (예: 이 요청 처리에 몇 ms 걸렸는지 로그를 남긴다, 권한을 확인한다)

원리: 앞에 대리자를 세운다​

비즈니스 메서드마다 공통 코드를 반복하지 않고 적용할 수 있다는 점이 AOP의 장점입니다. 이를 실현하는 방법은 여러 가지인데, 스프링은 그중 프록시 패턴(proxy pattern)을 씁니다. 진짜 객체 앞에 대리자(proxy) 객체를 하나 세워두고, 요청이 진짜 객체에 닿기 전과 후에 부가 기능을 대신 실행하는 방식입니다. (AspectJ처럼 컴파일·로드 시점에 바이트코드를 직접 고쳐 넣는 위빙(weaving) 방식도 있습니다.)

요청 ──▶ [프록시] ──(부가 기능: 로그·권한)──▶ [진짜 객체: 비즈니스 로직] ──▶ 응답

스프링은 이 대리자 객체를 런타임에 자동으로 만들어 진짜 객체 대신 연결합니다. 뒤에서 볼 @Transactional이 이 방식으로 동작합니다.




요청은 어떻게 처리되는가: 웹 레이어​

서두에서 처음 마주친 @RestController 파일로 돌아가, 요청이 실제로 어떻게 컨트롤러 메서드까지 도달하는지 따라가 보겠습니다.


모든 요청은 한 문 앞에 줄을 섭니다​

스프링으로 들어오는 모든 HTTP 요청은 먼저 DispatcherServlet이라는 단일 진입점을 거칩니다. DispatcherServlet이 요청을 받아 "이건 누가 처리해야 하지?"를 판단하고 알맞은 담당자에게 넘깁니다. 이런 구조를 프론트 컨트롤러(front controller) 패턴이라 부릅니다.

경로에 맞는 처리자로 넘긴다는 점은 React Router나 Express의 라우터와 비슷합니다. 서버 앞단에서는 DispatcherServlet이 이 역할을 맡습니다.


라우팅은 어노테이션으로 선언합니다​

DispatcherServlet은 요청의 URL과 HTTP 메서드를 보고, 그에 맞는 어노테이션이 붙은 컨트롤러 메서드로 요청을 넘깁니다. 이 매핑이 곧 라우팅입니다.

@RestController
@RequestMapping("/users") // 이 컨트롤러의 공통 경로 접두사
class UserController(
private val userService: UserService, // DI로 주입받는다
) {
// GET /users/42
@GetMapping("/{id}")
fun getUser(@PathVariable id: Long): UserResponse =
userService.findById(id)

// POST /users (요청 본문의 JSON이 CreateUserRequest 객체로 변환되어 들어온다)
@PostMapping
fun createUser(@RequestBody request: CreateUserRequest): UserResponse =
userService.create(request)
}

여기서 반환값(UserResponse)은 스프링이 알아서 JSON으로 직렬화해 HTTP 응답 본문에 실어 보냅니다. @RestController가 바로 "반환값을 그대로 응답 본문으로 만들라"는 의미이기 때문입니다. (전통적인 @Controller는 반환값을 렌더링할 뷰(HTML 템플릿)의 이름으로 해석합니다. API 서버를 만들 때는 대부분 @RestController를 씁니다.)

요청에서 값을 꺼내는 어노테이션들도 프론트엔드의 익숙한 개념에 그대로 대응됩니다.

스프링하는 일Express로 치면
@PathVariable경로의 일부(/users/{id})를 꺼냄req.params
@RequestParam쿼리스트링(?page=2)을 꺼냄req.query
@RequestBody요청 본문(JSON)을 객체로 변환req.body
메서드의 반환값JSON 응답 본문으로 변환res.json(...)

@RestController가 붙은 파일은 결국 Express의 라우트 핸들러 모음이나 Next.js의 route handler처럼 경로별로 "이 요청이 오면 이 함수를 실행한다"를 모아둔 파일입니다.




데이터 다루기: JPA와 트랜잭션​

대부분의 백엔드가 결국 하는 일은 데이터베이스와 대화하는 것입니다. 스프링에서 가장 흔한 방식이 Spring Data JPA입니다.


JPA는 백엔드의 ORM입니다​

JPA(현재 이름은 Jakarta Persistence, 이전에는 Java Persistence API)는 자바·코틀린 객체와 DB 테이블을 매핑해주는 표준이고, 그 대표 구현체가 하이버네이트(Hibernate)입니다. 역할로만 보면 프론트엔드에서 만난 Prisma나 TypeORM과 같은 ORM입니다. SQL을 직접 쓰는 대신 객체를 다루면 라이브러리가 SQL로 번역해줍니다.

// 이 클래스는 DB 테이블의 한 행(row)에 대응된다는 표시
@Entity
class User(
@Id @GeneratedValue
val id: Long = 0,
val email: String, // 각 필드가 곧 테이블의 컬럼이 된다
val name: String,
)

@Entity가 붙은 클래스는 테이블, 필드는 컬럼에 대응됩니다. Prisma 스키마에서 모델 하나를 정의하는 것에 해당합니다.


리포지토리는 인터페이스만 선언합니다​

DB를 실제로 읽고 쓰는 코드는 놀랍게도 인터페이스를 선언하기만 하면 됩니다. 구현체는 스프링이 런타임에 자동으로 만들어 빈으로 등록해줍니다.

interface UserRepository : JpaRepository<User, Long> {
// 메서드 이름의 규칙만으로 쿼리가 자동 생성된다 (SELECT ... WHERE email = ?)
fun findByEmail(email: String): User?
}

JpaRepository를 상속하는 것만으로 findById, save, delete 같은 기본 CRUD가 공짜로 딸려 옵니다. 게다가 findByEmail처럼 이름만 규칙에 맞게 지으면 그에 맞는 쿼리를 스프링이 만들어줍니다. 우리가 직접 구현하지 않은 인터페이스가 동작하는 이유 역시 앞서 본 컨테이너와 DI 덕분입니다.


@Transactional은 AOP의 대표 사례입니다​

주문을 하나 처리할 때 재고를 줄이고, 결제를 기록하고, 배송을 예약한다고 해봅시다. 이 중 하나라도 실패하면 앞의 작업들도 전부 없던 일로 되돌려야 합니다. 이렇게 여러 DB 작업을 "전부 성공 아니면 전부 취소(롤백)"로 묶는 것이 트랜잭션이고, 스프링에서는 메서드 위에 @Transactional 한 줄로 표현합니다.

@Service
class OrderService(/* ... */) {

@Transactional // 이 메서드 안의 DB 작업을 하나로 묶는다: 중간에 예외가 나면 전부 롤백
fun placeOrder(command: PlaceOrder) {
reduceStock(command)
recordPayment(command)
scheduleDelivery(command)
}
}

그런데 이 어노테이션 한 줄이 어떻게 "성공하면 커밋, 실패하면 롤백"을 알아서 해줄까요? 바로 앞서 본 AOP입니다. 스프링은 OrderService 앞에 프록시를 세워, 메서드 실행 전에 트랜잭션을 열고 정상 종료되면 커밋하고 롤백 규칙에 해당하는 예외가 나오면 롤백합니다. 기본적으로 RuntimeException과 Error가 롤백 대상이며 checked exception은 별도 설정이 필요합니다. 같은 객체 내부의 자기 호출은 기본 프록시 방식을 거치지 않으며, 외부 결제 API 호출도 DB 트랜잭션만으로 취소되지 않습니다. 트랜잭션 관리라는 횡단 관심사가 비즈니스 로직에서 완전히 분리되어 어노테이션 한 줄로 붙는다는 점에서, 실무에서 AOP의 효과를 가장 잘 보여 주는 사례입니다.




설정과 프로파일​

DB 주소, 포트, 외부 API 키처럼 환경마다 달라지는 값은 코드에 박아두면 안 됩니다. 스프링 부트는 이런 값을 application.yml(또는 application.properties)이라는 중앙 설정 파일에 모읍니다.

# application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb

프론트엔드의 .env에 해당하지만, 평평한 키·값이 아니라 계층 구조를 갖는다는 점이 다릅니다. 이렇게 모아둔 값은 코드에서 꺼내 쓸 수 있습니다.

@Service
class PaymentService(
// 설정 파일의 값을 주입받는다 (개별 값)
@Value("\${payment.api-key}") private val apiKey: String,
) { /* ... */ }

개별 값은 @Value로, 서로 관련된 값 묶음은 @ConfigurationProperties로 타입이 있는 객체에 한 번에 담아 쓰는 것이 보통입니다.


프로파일: 환경별로 설정을 가른다​

개발·스테이징·운영 환경은 DB도 키도 다릅니다. 스프링은 이를 프로파일(profile) 로 가릅니다. application-dev.yml, application-prod.yml처럼 환경별 파일을 두고, 앱을 실행할 때 어떤 프로파일을 쓸지 지정하면 기본 설정에 더해 그 환경의 설정이 적용됩니다.

환경마다 다른 설정을 적용한다는 점은 프론트엔드의 .env.development / .env.production과 비슷합니다.




테스트​

앞의 DI 예제에서는 생성자에 가짜 구현을 넣었습니다. 스프링 컨테이너나 HTTP 요청 처리까지 검증할 때는 아래 도구들을 씁니다. spring-boot-starter-test를 추가하면 JUnit·Mockito 등을 함께 가져올 수 있습니다.

  • @SpringBootTest: 실제 스프링 컨테이너를 통째로 띄워서 하는 통합 테스트입니다. 앱 전체를 실행해 검증하므로 든든하지만 무겁습니다. 프론트엔드에서 앱 전체를 마운트해 돌리는 통합 테스트에 해당합니다.
  • MockMvc: 진짜 서버(네트워크)를 띄우지 않고 HTTP 요청을 흉내 내 컨트롤러를 검증합니다. supertest나 MSW로 요청을 흉내 내 라우트를 테스트하는 것과 같습니다.
  • @MockitoBean(Spring Boot의 @MockBean을 대체하는 Spring Framework API): 컨테이너의 특정 빈을 가짜로 교체합니다. 예를 들어 진짜 결제 API를 호출하는 빈을 가짜로 바꿔두면, 실제 결제 없이 로직만 검증할 수 있습니다. jest.mock·vi.mock으로 의존성을 교체하는 것에 해당합니다.



스프링 프레임워크와 스프링 부트​

여기까지 빈·DI·AOP·웹·데이터·설정·테스트를 하나씩 봤습니다. 마지막으로, 이 모든 것을 실제 프로젝트로 묶어 손쉽게 시작하게 해주는 것이 스프링 부트(Spring Boot)입니다. 실무에서 프로젝트를 열면 대개 만나는 이름도 "스프링"이 아니라 "스프링 부트"입니다.


스프링 부트가 없던 시절​

과거의 스프링 프레임워크는 스무 개 남짓한 모듈로 이루어진 크고 무거운 도구 모음이었습니다. 웹 기능을 하려면 Spring MVC를, DB를 연결하려면 Spring JDBC나 ORM 모듈을 일일이 가져와 서로 호환되는 버전으로 맞추고, 방대한 설정을 손수 작성해야 했습니다. 이 귀찮은 설정 과정을 알아서 자동화(auto-configuration)해주는 것이 스프링 부트입니다.

프론트엔드에서 Next.js나 Vite가 프로젝트 설정과 개발·빌드 도구 구성을 묶어 제공하는 것과 비슷합니다. 스프링 부트는 백엔드 프로젝트의 의존성과 설정을 같은 방식으로 묶습니다.


스타터(Starter): 검증된 세트를 한 번에​

스프링 부트의 핵심 도구는 스타터(Starter)입니다. spring-boot-starter-... 모듈 하나만 가져오면, 그 목적에 필요한 라이브러리들이 호환성이 검증된 버전 세트로 함께 딸려 옵니다. 개별 라이브러리 버전을 손으로 맞출 일이 사라지는 것입니다.

백엔드 코드의 build.gradle 파일에서 아래 의존성들을 만나면, 각각 이런 역할을 하는구나 하고 읽으시면 됩니다. 이제 각 스타터가 다루는 개념은 앞 단원에서 이미 만난 것들입니다.

스타터 이름역할이 글에서 다룬 곳 · FE로 치면
spring-boot-starter-webREST API 서버의 핵심 (라우팅, HTTP 요청 처리, 내장 웹서버)웹 레이어 · 서버 라우터 + 요청 핸들링
spring-boot-starter-data-jpa코드로 DB를 다루게 해주는 ORMJPA와 트랜잭션 · Prisma, TypeORM
spring-boot-starter-security인증·인가와 라우트 접근 제어인증 미들웨어
spring-boot-starter-test단위·통합 테스트 도구 모음테스트 · Jest, Vitest



FE ↔ 스프링 대응표​

이 글에서 다룬 대응을 한자리에 모읍니다.

스프링 개념FE에서 가장 가까운 것대응의 핵심
느슨한 결합 · 컴포넌트 아키텍처잘게 쪼갠 React 컴포넌트독립된 블록의 조립, 높은 테스트 용이성
빈(Bean) · 컨테이너모듈 스코프의 공유 싱글턴 (예: apiClient)프레임워크가 관리하는 객체
어노테이션 (@Service 등)데코레이터 (Angular · NestJS)얹어서 메타데이터·동작을 부여
의존성 주입(DI)props로 넘기는 의존·콜백스스로 만들지 않고 바깥에서 받는다
제어의 역전(IoC) · 컨테이너컴포넌트 생명주기를 관리하는 React 런타임생성·연결·수명의 통제권이 프레임워크로
관점 지향(AOP) · 프록시Axios 인터셉터 · 고차 컴포넌트(HOC)원본을 고치지 않고 공통 관심사를 감싼다
DispatcherServlet · 라우팅React Router · Express 앱요청을 알맞은 핸들러로 넘기는 단일 문
@RestController 핸들러Express 라우트 핸들러 · Next.js route handler경로별 처리 함수의 모음
JPA · @EntityPrisma · TypeORM객체와 테이블을 매핑하는 ORM
@Transactional(AOP로 구현된) 트랜잭션 경계전부 성공 아니면 전부 롤백
application.yml · 프로파일.env · .env.production환경별 설정 분리
@SpringBootTest · MockMvc · @MockitoBeanVitest 통합 테스트 · supertest · vi.mockDI가 열어주는 테스트 용이성
스프링 부트 · 스타터Next.js · Vite의 프로젝트 도구 구성설정과 호환되는 의존성 묶음 제공

자바·백엔드 진영의 용어는 낯설어도 "유지보수하기 좋게 모듈화하고, 인터페이스로 결합도를 낮추며, 테스트하기 쉽게 만든다"는 설계 원칙은 프론트엔드에서 익힌 것과 많이 닮았습니다. 이제 회사 코드베이스에서 @RestController가 붙은 파일 하나를 열어보시면, 위 구조가 어떻게 구현되어 있는지 조금 더 또렷하게 보이실 것입니다.




References​

스프링 공식 문서

  1. Spring Framework: Core Technologies (IoC Container & Beans)
  2. Spring Framework: Aspect Oriented Programming with Spring
  3. Spring Framework: Web on Servlet Stack (DispatcherServlet)
  4. Spring Data JPA: Reference Documentation
  5. Spring Framework: Transaction Management
  6. Spring Boot: Reference Documentation
  7. Spring Boot: Testing
  8. Spring Boot Starters 목록

개념 배경

  1. Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern
  2. Martin Fowler: Inversion of Control
  3. Wikipedia: Cross-cutting concern
  4. Wikipedia: Front controller pattern


좋은 사람들과 재미있는 일을 하며 열정적이고 즐겁게 살고 싶은 개발자