Spring은 Java로 웹 애플리케이션을 개발할 때 많이 사용하는 프레임워크다.
처음 Spring을 보면 @Controller, @Service, @Repository, @Autowired, @Transactional처럼 생소한 어노테이션이 계속 등장한다. 여기에 Spring MVC, JPA, Security, AOP까지 붙으면서 각각의 기능을 따로 외우기 쉽다.
하지만 Spring을 이해할 때 먼저 봐야 하는 것은 어노테이션이 아니다.
객체가 어떻게 만들어지고, 서로 어떻게 연결되며, HTTP 요청이 애플리케이션 내부를 어떻게 통과하는가를 이해하는 것이 먼저다.
Spring 웹 애플리케이션의 기본적인 흐름은 다음과 같이 볼 수 있다.
HTTP Request -> DispatcherServlet -> Controller -> Service -> Repository -> Database
그리고 이 과정에서 필요한 객체들을 Spring Container가 생성하고 관리한다.
Spring은 무엇을 해결하는가
Java에서는 필요한 객체를 직접 생성할 수 있다.
UserRepository repository = new UserRepository();
UserService service = new UserService(repository);
작은 프로그램에서는 문제가 없다.
하지만 애플리케이션이 커지면 객체의 수가 많아지고 서로 의존하는 관계도 복잡해진다.
OrderController -> OrderService -> OrderRepository -> Database
여기에 인증, 로깅, 트랜잭션 같은 공통 기능까지 추가되면 객체 생성과 연결을 직접 관리하는 일이 점점 복잡해진다.
Spring은 이런 객체의 생성과 관리, 의존성 연결, 웹 요청 처리, 트랜잭션, 보안 등의 기능을 프레임워크 차원에서 제공한다.
개발자는 객체를 직접 생성하는 코드보다 각 객체의 책임과 의존관계를 설계하는 데 집중할 수 있다.
Bean과 Spring Container
Spring에서 자주 등장하는 개념이 Bean이다.
Bean은 Spring Container가 생성하고 관리하는 객체다.
@Service
public class UserService {
}
UserService에 @Service가 붙으면 Spring이 해당 클래스를 Bean으로 등록하고 관리할 수 있다.
대표적인 Bean 등록 방식은 다음과 같다.
@Component
@Service
@Repository
@Controller
@RestController
직접 Bean을 등록하는 것도 가능하다.
@Configuration
public class AppConfig {
@Bean
public UserService userService() {
return new UserService();
}
}
Spring 애플리케이션이 실행되면 Container가 필요한 Bean을 생성하고 관리한다.
Bean 사이에 의존관계가 있다면 필요한 객체를 연결해준다.
Spring Container -> UserRepository Bean -> UserService Bean -> UserRepository Dependency
여기서 중요한 것은 @Service라는 어노테이션 자체가 아니다.
왜 이 객체를 Spring이 관리해야 하는가를 이해하는 것이 핵심이다.
Dependency Injection
객체가 다른 객체를 필요로 하는 관계를 의존관계라고 한다.
예를 들어 UserService가 UserRepository를 사용한다고 하자.
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
}
UserService는 UserRepository를 직접 생성하지 않는다.
필요한 객체를 외부에서 전달받는다.
이것이 Dependency Injection, DI다.
Spring에서는 일반적으로 생성자 주입을 사용한다.
@Service
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
}
Spring Container가 UserRepository Bean을 찾아 생성자를 통해 전달한다.
Spring Container -> UserRepository 생성 -> UserService 생성 -> Repository 주입
이렇게 하면 UserService가 구체적인 구현 객체를 직접 생성하지 않아도 된다.
테스트에서도 필요한 객체를 직접 전달할 수 있다.
UserRepository repository = new FakeUserRepository();
UserService service = new UserService(repository);
객체를 생성하는 책임과 객체를 사용하는 책임을 분리할 수 있다는 것이 DI의 중요한 장점이다.
IoC
DI를 이해하면 IoC도 자연스럽게 연결된다.
일반적인 Java 코드에서는 개발자가 객체의 생성과 연결을 직접 결정한다.
개발자 -> 객체 생성 -> 객체 연결 -> 객체 사용
Spring을 사용하면 객체의 생성과 연결을 Spring Container가 담당한다.
Spring Container -> 객체 생성 -> 객체 연결 -> 객체 관리
개발자가 가지고 있던 제어권 일부가 프레임워크로 넘어간다.
이것을 IoC, Inversion of Control이라고 한다.
DI는 이러한 IoC를 구현하는 대표적인 방법이다.
따라서 두 개념을 같은 것으로 보기보다는 다음과 같이 이해하는 것이 좋다.
IoC -> 제어의 역전
IoC -> DI를 포함한 여러 방식으로 구현 가능
DI -> 객체의 의존성을 외부에서 주입
Spring MVC
Spring으로 웹 애플리케이션을 개발하면 Spring MVC를 만나게 된다.
HTTP 요청이 들어오면 일반적으로 다음과 같은 흐름을 거친다.
Client -> HTTP Request -> DispatcherServlet -> Controller -> Service -> Repository -> Database
여기서 DispatcherServlet은 Spring MVC의 중심적인 역할을 담당한다.
HTTP 요청을 받아 적절한 Controller로 전달하고 Controller의 처리 결과를 HTTP 응답으로 연결한다.
Controller는 웹 요청을 처리하는 입구에 가깝다.
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/users/{id}")
public UserResponse getUser(@PathVariable Long id) {
return userService.getUser(id);
}
}
Controller에서 데이터베이스에 직접 접근하기보다 Service를 호출하는 구조를 많이 사용한다.
Controller와 Service
Controller와 Service를 분리하는 이유는 책임을 나누기 위해서다.
Controller에서 모든 로직을 처리해도 프로그램 자체는 동작할 수 있다.
@GetMapping("/users/{id}")
public UserResponse getUser(@PathVariable Long id) {
User user = userRepository.findById(id);
// 여러 비즈니스 로직
return UserResponse.from(user);
}
하지만 애플리케이션이 커지면 Controller가 지나치게 많은 책임을 가지게 된다.
그래서 보통 다음과 같이 역할을 분리한다.
Controller -> HTTP 요청과 응답 처리
Service -> 비즈니스 로직 처리
Repository -> 데이터 접근
회원 가입을 예로 들면:
POST /users -> UserController -> UserService -> UserRepository -> Database
Controller는 HTTP 요청을 받고 Service를 호출한다.
Service는 중복 회원 확인, 회원 생성 같은 비즈니스 로직을 처리한다.
Repository는 데이터를 저장하거나 조회한다.
이렇게 역할을 나누면 각 클래스가 담당하는 범위가 명확해진다.
Repository와 Database
Repository는 데이터의 저장과 조회를 담당하는 계층이다.
Spring에서 데이터베이스를 다룰 때는 JDBC를 직접 사용할 수도 있고 JPA 같은 ORM을 사용할 수도 있다.
Spring Data JPA를 사용하면 Repository 구현을 상당 부분 자동화할 수 있다.
public interface UserRepository
extends JpaRepository<User, Long> {
}
기본적인 CRUD 기능을 제공받을 수 있다.
userRepository.findById(id);
userRepository.findAll();
userRepository.save(user);
userRepository.delete(user);
여기서 JPA와 Hibernate, Spring Data JPA를 같은 기술로 생각하면 안 된다.
각각의 역할이 다르다.
Spring Data JPA -> Repository 사용 편의 기능
JPA -> Java ORM 표준
Hibernate -> JPA 구현체
JDBC -> Java Database 연결 API
Database -> 실제 데이터 저장소
전체적인 관계는 다음과 같이 볼 수 있다.
Application -> Spring Data JPA -> JPA -> Hibernate -> JDBC -> Database
각 기술이 어느 계층에 위치하는지 알고 있으면 이름이 비슷한 기술들이 훨씬 덜 헷갈린다.
Entity와 DTO
JPA를 사용하면 Entity를 만들게 된다.
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
private String email;
}
Entity는 데이터베이스 테이블과 매핑되는 객체다.
하지만 API 요청이나 응답에 Entity를 그대로 사용하는 것은 주의해야 한다.
회원 생성 요청을 받는다면 별도의 DTO를 사용할 수 있다.
public class CreateUserRequest {
private String name;
private String email;
}
요청 데이터는 Service에서 Entity로 변환할 수 있다.
HTTP Request -> Request DTO -> Service -> Entity -> Repository -> Database
응답도 별도의 DTO로 변환할 수 있다.
Database -> Entity -> Service -> Response DTO -> HTTP Response
이렇게 하면 API의 데이터 구조와 데이터베이스의 구조를 분리할 수 있다.
Entity에 필드를 추가했다고 해서 API 응답 구조가 반드시 변경되는 구조를 피할 수 있다는 장점도 있다.
Transaction
데이터베이스 작업을 하나의 작업 단위로 처리해야 할 때 Transaction을 사용한다.
예를 들어 주문을 처리하면서 주문을 저장하고 재고를 감소시킨다고 하자.
@Transactional
public void order() {
orderRepository.save(order);
inventory.decrease();
}
두 작업이 모두 정상적으로 처리되어야 한다면 하나의 Transaction으로 묶을 수 있다.
정상적으로 완료되면 Commit한다.
Transaction Start -> 주문 저장 -> 재고 감소 -> 정상 종료 -> Commit
중간에 오류가 발생하면 Rollback할 수 있다.
Transaction Start -> 주문 저장 -> 재고 감소 실패 -> Rollback
Spring의 @Transactional은 단순한 DB 편의 기능이 아니다.
Spring의 Transaction 관리와 Proxy 기반 AOP 구조가 연결되어 있다.
따라서 이후 AOP를 공부하면 @Transactional의 동작 구조도 더 명확하게 이해할 수 있다.
AOP
애플리케이션에는 여러 클래스에서 반복적으로 필요한 기능이 있다.
대표적으로 다음과 같은 것들이 있다.
Logging
Transaction
Performance Measurement
Security
이런 기능을 모든 메서드에 직접 작성하면 비즈니스 로직과 공통 기능이 섞인다.
public void createUser() {
log.info("start");
// 실제 비즈니스 로직
log.info("end");
}
AOP는 이러한 공통 관심사를 별도로 분리할 수 있도록 한다.
Business Logic + Cross-cutting Concern
Spring에서는 Proxy를 이용한 AOP가 많이 사용된다.
Client -> Proxy -> Target Object -> Business Logic
@Transactional 역시 이러한 Proxy 구조와 연결된다.
AOP를 처음부터 깊게 파고들 필요는 없지만, 반복되는 공통 기능을 비즈니스 로직과 분리하기 위한 구조라는 점은 이해하고 넘어가는 것이 좋다.
Spring Security
웹 애플리케이션에서는 로그인과 권한 처리가 필요하다.
Spring에서는 Spring Security를 사용할 수 있다.
먼저 인증과 인가를 구분해야 한다.
Authentication -> 사용자가 누구인지 확인
Authorization -> 사용자가 무엇을 할 수 있는지 확인
Spring Security는 Filter 기반 구조를 사용해 Controller에 요청이 도달하기 전에 보안 관련 처리를 수행할 수 있다.
대략적인 흐름은 다음과 같다.
HTTP Request -> Security Filter Chain -> Authentication / Authorization -> DispatcherServlet -> Controller
예를 들어 인증이 필요한 API라면 Controller가 실행되기 전에 인증 정보를 확인할 수 있다.
JWT나 OAuth2 같은 인증 방식도 Spring Security와 함께 사용할 수 있다.
중요한 것은 인증과 인가를 Client에서만 처리해서는 안 된다는 것이다.
Client UI에서 버튼 숨김 -> 보안 기능 아님
Server에서 인증 확인 -> 필요
Server에서 권한 확인 -> 필요
실제 데이터 접근이나 API, Service, Server Action에 해당하는 서버 경계에서도 필요한 권한 검증이 이루어져야 한다.
Spring Boot
Spring과 Spring Boot는 구분해서 볼 필요가 있다.
Spring은 애플리케이션 개발을 위한 프레임워크이고, Spring Boot는 Spring 애플리케이션을 빠르게 구성하고 실행할 수 있도록 지원하는 프로젝트다.
Spring Boot에서는 많은 설정이 자동으로 구성된다.
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
또한 내장 서버를 사용할 수 있기 때문에 별도의 외부 서버를 직접 구성하지 않아도 애플리케이션을 실행할 수 있다.
의존성에 따라 필요한 설정을 자동으로 구성하는 Auto Configuration도 Spring Boot의 핵심 기능 중 하나다.
실제 Spring 프로젝트에서는 대부분 Spring Boot를 기반으로 애플리케이션을 구성한다.
Spring에서 자주 사용하는 Annotation
Spring을 공부하다 보면 많은 Annotation을 만나게 된다.
처음부터 전부 외우기보다 역할을 기준으로 묶어 보는 편이 좋다.
Bean 등록
@Component
@Service
@Repository
@Controller
@RestController
Spring Container가 관리할 객체를 등록하는 데 사용한다.
Dependency Injection
@Autowired
의존성을 주입할 때 사용할 수 있다.
다만 최근에는 생성자 주입을 주로 사용하기 때문에 생성자가 하나라면 @Autowired를 직접 작성하지 않는 경우가 많다.
HTTP Request
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@RequestBody
@PathVariable
@RequestParam
Controller에서 HTTP 요청을 처리할 때 사용한다.
Configuration
@Configuration
@Bean
@Value
Spring의 설정과 Bean을 구성할 때 사용한다.
Transaction
@Transactional
Transaction 경계를 설정할 때 사용한다.
Annotation을 단순히 암기하기보다 어느 계층에서 어떤 목적으로 사용하는가를 기준으로 보는 것이 좋다.
전체적인 Spring 구조
지금까지의 내용을 하나로 연결하면 다음과 같은 흐름이 된다.
Client -> HTTP Request -> Security Filter Chain -> DispatcherServlet -> Controller -> Service -> Repository -> JPA / Hibernate -> JDBC -> Database
그리고 애플리케이션에서 사용하는 Bean들은 Spring Container가 관리한다.
Spring Container -> Controller Bean -> Service Bean -> Repository Bean
Bean 사이의 의존성도 Spring이 연결한다.
Spring Container -> Dependency Injection -> Object Graph 구성
Transaction과 AOP 같은 기능은 객체 실행 과정에 개입할 수 있다.
Client -> Proxy -> Target Bean -> Business Logic
이 구조를 알고 있으면 각각의 Spring 기능이 서로 떨어진 기능이 아니라 하나의 애플리케이션 흐름 안에서 연결되어 있다는 것을 볼 수 있다.
Spring에서 함께 알아야 할 것
Spring만 공부해서 웹 개발 전체를 이해하기는 어렵다.
Spring은 여러 기술을 연결해서 웹 애플리케이션을 구성하는 프레임워크이기 때문이다.
기본적으로 다음 기술들을 함께 이해할 필요가 있다.
Java -> Object-Oriented Programming -> HTTP -> Servlet -> Spring MVC -> SQL / Database -> JPA -> Transaction -> Spring Security
특히 Java의 객체지향 개념이 부족하면 Spring의 DI와 IoC도 Annotation 사용법 정도로만 이해하기 쉽다.
JPA를 사용할 경우에는 SQL과 관계형 데이터베이스의 구조도 알아야 한다.
Spring이 대신 처리하는 부분과 실제 기반 기술이 처리하는 부분을 구분해서 보는 것도 중요하다.
Spring을 하나의 흐름으로 보면
Spring의 핵심은 크게 두 가지 흐름으로 볼 수 있다.
첫 번째는 객체 관리다.
Configuration -> Bean 생성 -> Dependency Injection -> Spring Container에서 관리
두 번째는 HTTP 요청 처리다.
HTTP Request -> Security Filter Chain -> DispatcherServlet -> Controller -> Service -> Repository -> Database
이 둘이 결합해서 하나의 Spring 애플리케이션을 구성한다.
HTTP Request -> Spring MVC -> Controller -> Service -> Repository -> Database
^
|
Spring Container
Bean / DI / AOP
여기에 데이터 변경이 필요하면 Transaction이 붙고, 인증과 권한이 필요하면 Security가 요청 처리 앞단에 붙는다.
Client -> Security -> Spring MVC -> Controller -> Service -> Transaction -> Repository -> Database
Spring과 Spring Boot의 관계
전체 구조를 정리하면 다음과 같이 볼 수 있다.
Spring Boot -> Spring Framework -> Spring MVC / DI / AOP / Transaction / Security Integration
실제 프로젝트에서는 Spring Boot가 애플리케이션의 실행과 설정을 편리하게 만들어주고, 그 위에서 Spring의 여러 기능을 사용한다.
따라서 Spring을 공부한다는 것은 단순히 @Controller나 @Service를 사용하는 방법을 배우는 것이 아니다.
Java Object -> Spring Container -> Dependency Injection -> HTTP Request -> Spring MVC -> Business Logic -> Database
이 전체 흐름을 이해하는 것이 핵심이다.
정리
Spring을 처음 보면 수많은 Annotation과 기능 때문에 복잡해 보인다.
하지만 핵심 구조를 잡으면 크게 복잡하지 않다.
Spring Container가 객체를 생성하고 관리한다.
Bean Definition -> Bean Creation -> Dependency Injection -> Object Management
Spring MVC는 HTTP 요청을 애플리케이션으로 연결한다.
HTTP Request -> DispatcherServlet -> Controller
애플리케이션 내부에서는 역할을 나눠서 처리한다.
Controller -> Service -> Repository
Repository는 JPA 등의 기술을 이용해 데이터베이스와 통신한다.
Repository -> Spring Data JPA -> JPA -> Hibernate -> JDBC -> Database
Transaction과 AOP는 객체 실행 과정에 개입하고, Security는 요청 처리 과정에서 인증과 인가를 담당한다.
Request -> Security -> Controller -> Service -> Transaction -> Repository -> Database
결국 Spring에서 가장 먼저 이해해야 할 것은 Annotation 목록이 아니다.
객체가 어떻게 생성되고, 서로 어떻게 연결되며, HTTP 요청 하나가 어떤 계층을 거쳐 데이터베이스까지 도달하는가를 이해하는 것이 먼저다.
그 구조를 이해하면 @Service, @Repository, @Transactional, @Autowired 같은 Annotation도 단순히 외워야 하는 문법이 아니라 객체 구조와 실행 흐름의 특정 지점에 붙는 기능으로 보이기 시작한다.
Spring을 하나의 흐름으로 압축하면 다음과 같다.
HTTP Request -> Security -> DispatcherServlet -> Controller -> Service -> Repository -> Database
그리고 이 객체들을 구성하고 연결하는 기반에는 다음 구조가 있다.
Spring Container -> Bean -> Dependency Injection -> Object Graph
결국 Spring 개발의 핵심은 객체의 책임과 의존관계를 설계하고, HTTP 요청이 그 객체들을 어떤 순서로 통과하는지를 관리하는 것이다.