Spring Framework

백엔드 동작 원리

SPRING = 식당 운영 시스템

Spring Framework는 “식당”이라고 생각하면 된다. 손님(HTTP 요청)이 들어오면 이 순서로 처리한다.

손님HTTP 요청
식당 문Tomcat
주방장DispatcherServlet
주문 접수Controller
요리사Service
재료 창고Repository
냉장고DB

각 역할이 차지하는 비중

주문 접수

Controller — 요청 받고, 응답 돌려주기
15%

요리사

Service — 실제 요리(비즈니스 로직)
40%

재료 창고

Repository — 냉장고(DB)에서 재료 꺼내기
30%

식당 운영

Infrastructure — 보안, 로깅, 청소
15%
1

DI (의존성 주입)

Spring의 핵심 — 필요한 도구를 알아서 가져다 준다

프론트에서 props로 컴포넌트에 데이터를 넘기는 것과 비슷하다. Spring에서는 “이 클래스가 저 클래스를 필요로 하는구나”를 자동으로 파악해서 넣어준다.

🔌
DI (Dependency Injection)= “콘센트에 플러그 꽂기

전자제품(클래스)이 전기(다른 클래스)가 필요할 때, 직접 발전기를 만들지 않고 그냥 콘센트에 꽂으면 된다. Spring이 콘센트 역할을 해서 필요한 전기를 알아서 연결해준다. React에서 Context로 전역 상태를 꽂아 쓰는 것과 비슷하다.

🔄
IoC (제어 역전)= “내가 안 만들고, 남이 만들어서 줌

보통은 '내가 필요한 걸 직접 new로 만든다'. IoC에서는 '프레임워크가 만들어서 나한테 넣어준다'. 마치 React에서 컴포넌트를 직접 render() 호출하지 않고 React가 알아서 호출하는 것과 같은 원리다.

Bean= “Spring이 관리하는 부품 하나하나

Spring이 만들고 관리하는 객체를 'Bean'이라 부른다. 식당에 비유하면 '정규직 직원' 같은 것. 알바(직접 new로 만든 객체)와 달리, 정규직(Bean)은 식당(Spring)이 채용하고, 배치하고, 퇴사시킨다. @Service, @Controller 같은 어노테이션을 붙이면 자동으로 Bean 등록.

전통적인 방식 (직접 만들기)

class 요리사 {
  // 칼을 직접 만든다 — 강결합
  칼 myKnife = new 칼();
  도마 myBoard = new 도마();

  // 칼이 바뀌면? 요리사 코드를 수정해야 함
  // 테스트할 때 진짜 칼을 써야 함
}

Spring DI (알아서 넣어줌)

@Service
class 요리사 {
  // Spring이 알아서 넣어줌 — 약결합
  final 칼 myKnife;    // ← 자동 주입
  final 도마 myBoard;   // ← 자동 주입

  // 칼을 바꿔도 요리사 코드 수정 불필요
  // 테스트 시 장난감 칼(Mock)로 교체 가능
}
2

HTTP 요청 흐름

손님이 주문하면 식당에서 벌어지는 일

프론트에서 fetch('/api/users')를 호출하면, 그 요청이 Spring 내부에서 어떤 여정을 거치는지 식당에 비유해서 알아보자.

🚪
Tomcat= “식당 입구 문

손님(HTTP 요청)이 처음 도착하는 곳. 문을 열고 들어오면 대기열에 세운다. Tomcat은 Spring 내장 웹 서버로, React의 dev server (next dev)처럼 HTTP를 받아들이는 역할이다. 차이점은 프로덕션에서도 이 서버를 그대로 쓴다는 것.

🚧
Filter= “입구 검문소

식당 문을 열고 들어오면 검문소를 여러 개 통과한다. '마스크 착용 확인(인코딩)', '회원증 확인(인증)', '예약 확인(인가)'. 프론트의 미들웨어(Next.js middleware)와 거의 같은 개념이다. 요청이 컨트롤러에 도달하기 전에 공통 처리를 한다.

👨‍💼
DispatcherServlet= “주방장 (총괄 매니저)

모든 주문이 주방장 한 명을 거친다. 주방장이 '이 주문은 비빔밥 담당에게!' 하고 적절한 셰프(Controller)에게 배분한다. Next.js로 치면 app router가 URL을 보고 어떤 page.tsx를 실행할지 결정하는 것과 같다.

📋
Controller= “주문 접수 카운터

주문서(요청)를 받아서 내용을 정리한다. '비빔밥 1개, 계란 추가'. 주문 내용이 이상하면 돌려보낸다('가격은 음수일 수 없습니다'). 프론트의 form validation과 비슷한 역할. 정리가 끝나면 요리사(Service)에게 넘긴다.

👨‍🍳
Service= “실제 요리하는 셰프

핵심 비즈니스 로직을 처리하는 곳. '재료가 충분한가?', '이 메뉴는 점심에만 가능한가?', '할인 쿠폰 적용' 같은 판단을 한다. 프론트에서 API 응답을 가공하는 유틸 함수들을 모아놓은 것과 비슷하지만, 훨씬 더 핵심적인 비즈니스 규칙을 담당한다.

🗄️
Repository= “재료 창고 담당자

셰프(Service)가 '토마토 3개 가져와'(findById) 하면 창고(DB)에서 찾아온다. 셰프가 직접 창고에 들어가지 않는다. 프론트에서 API 호출 함수를 별도 파일(api.ts)에 분리하는 것과 같은 패턴이다.

🏦
DB (데이터베이스)= “냉장고 + 금고

모든 재료(데이터)가 실제로 보관되는 곳. MySQL, PostgreSQL 같은 것. 프론트에서 localStorage나 IndexedDB에 데이터를 저장하는 것과 비슷하지만, 서버 쪽 DB는 수백만 건의 데이터를 안전하게 보관하고, 동시에 여러 명이 접근해도 꼬이지 않게 관리한다.

GET /api/users/1 — 전체 흐름

프론트엔드 — fetch('/api/users/1')

├→ Tomcat — 식당 문. HTTP 요청 수신

├→ Filter — 검문소. 인증/인가 확인

├→ DispatcherServlet — 주방장. “이건 User 담당!”

├→ UserController — 주문 접수. “id=1 유저 조회요”

└→ UserService — 요리사. “존재하는 유저인지 확인”

└→ UserRepository — 창고 담당. “DB에서 id=1 찾기”

└→ DB — SELECT * FROM users WHERE id = 1

데이터가 역순으로 돌아온다

├→ DB {id:1, name:"홍길동"}

├→ Repository → User 객체로 변환

├→ Service → UserDTO로 변환 (민감정보 제거)

├→ Controller → HTTP 200 + JSON 응답

└→ 프론트엔드 → res.json() → setState → UI 업데이트

3

3계층 구조

Controller → Service → Repository, 왜 이렇게 나눌까?

프론트에서 컴포넌트 / hooks / API 함수를 분리하는 것과 같은 이유다. 각자 맡은 일만 하면 코드가 깔끔하고, 한 곳이 바뀌어도 다른 곳에 영향이 없다.

📋
Controller= “React 컴포넌트 (UI 담당)

프론트의 페이지 컴포넌트가 '화면만 담당하고 로직은 훅에 위임'하듯이, Controller는 'HTTP 요청/응답만 담당하고 로직은 Service에 위임'한다. URL 매핑, 입력 검증, 응답 포맷팅만 하고, '어떻게 처리할지'는 모른다.

🧠
Service= “Custom Hook (로직 담당)

useCart(), useAuth() 처럼 비즈니스 로직을 담는 곳. '장바구니에 상품 추가 시 재고 확인 → 할인 계산 → 포인트 적립' 같은 진짜 업무 로직이 여기에 있다. HTTP가 뭔지 모르고, DB가 뭔지도 모른다. 오직 비즈니스 규칙만 안다.

📡
Repository= “API 호출 함수 (fetch 래퍼)

프론트에서 api/users.ts 파일에 fetchUser(), createUser() 함수를 모아놓듯이, Repository는 DB 접근 함수를 모아놓은 곳이다. 차이점은 SQL을 직접 안 쓰고 함수 이름만 정의하면 Spring이 자동으로 SQL을 만들어준다는 것.

Spring (백엔드)React/Next.js (프론트)하는 일
@Controllerpage.tsx (컴포넌트)요청/응답 담당, 화면 진입점
@ServiceuseXxx() (커스텀 훅)비즈니스 로직, 상태 관리
@Repositoryapi/xxx.ts (fetch 함수)외부 데이터 접근
EntityDB 스키마 타입DB 테이블과 1:1 매핑되는 객체
DTOAPI response 타입전달용 데이터 (민감정보 제거)
Filtermiddleware.ts요청 전처리 (인증, 로깅)

코드로 보면 이렇다

// 1. Controller — "주문 접수 카운터"
@RestController                          // → 이건 REST API 담당이야
@RequestMapping("/api/users")            // → /api/users로 시작하는 요청 담당
public class UserController {
    private final UserService userService;  // ← DI (Spring이 자동 주입)

    @GetMapping("/{id}")                  // → GET /api/users/1 요청이 오면
    public UserDTO getUser(@PathVariable Long id) {
        return userService.findById(id);   // → 요리사(Service)에게 위임
    }
}

// 2. Service — "요리하는 셰프"
@Service
public class UserService {
    private final UserRepository repo;     // ← DI

    public UserDTO findById(Long id) {
        User user = repo.findById(id)      // → 창고(Repository)에서 재료 가져오기
            .orElseThrow(() -> new RuntimeException("없는 유저"));
        return new UserDTO(user.getId(), user.getName());
        // Entity → DTO: 주방 재료를 손님용 그릇에 담아서 내보내기
    }
}

// 3. Repository — "재료 창고 담당자"
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // 이것만 쓰면 끝! 구현 코드를 안 써도 Spring이 알아서 SQL을 만들어준다
    // findById(1L) → SELECT * FROM users WHERE id = 1
    // findAll()    → SELECT * FROM users
    // save(user)   → INSERT INTO users ...
    // delete(user) → DELETE FROM users WHERE id = ...
}
4

@ 어노테이션

Spring의 마법 주문 — 코드 위에 붙이면 자동으로 동작한다

프론트에서 'use client'를 파일 맨 위에 붙이면 클라이언트 컴포넌트가 되는 것처럼, Spring에서는 @로 시작하는 어노테이션을 붙이면 특별한 능력이 부여된다.

🏷️
@RestController= “'use client' 같은 것

이 클래스가 'REST API를 처리하는 컨트롤러'라고 선언한다. 이걸 붙이면 Spring이 이 클래스를 자동으로 찾아서 URL 요청을 연결해준다. 'use client'를 붙이면 React가 클라이언트 컴포넌트로 인식하는 것과 같은 원리.

🗺️
@GetMapping / @PostMapping= “Next.js의 app/api/route.ts

어떤 HTTP 메서드(GET, POST)와 URL 경로를 이 함수가 처리할지 정의한다. Next.js에서 export async function GET(req) 하는 것과 완전히 같은 역할. @GetMapping('/users') = GET /users 요청을 이 함수가 처리한다.

📩
@RequestBody= “req.json() 자동화

프론트에서 보낸 JSON body를 Java 객체로 자동 변환해준다. Next.js API 라우트에서 const body = await req.json() 하는 것을 어노테이션 하나로 끝낸다. @RequestBody UserDTO dto → JSON이 UserDTO 객체가 된다.

🔗
@PathVariable= “Next.js의 [id] 동적 라우트

/api/users/[id]에서 id 값을 꺼내는 것. Next.js에서 params.id로 꺼내듯, Spring에서는 @PathVariable Long id로 꺼낸다. /api/users/42 요청이 오면 id = 42가 된다.

💳
@Transactional= “카드 결제 — 전부 성공하거나 전부 취소

편의점에서 삼각김밥 + 음료를 사는데, 삼각김밥은 결제됐는데 음료에서 오류나면? @Transactional은 '둘 다 취소(ROLLBACK)'시킨다. 전부 성공해야만 확정(COMMIT). 계좌이체에서 A통장 출금은 됐는데 B통장 입금이 실패하면 안 되는 것과 같다.

@Valid= “form validation을 백엔드에서도

프론트에서 '이메일 형식이 아닙니다', '이름은 필수입니다' 하는 검증을 백엔드에서도 한다. 왜? 프론트 검증은 개발자 도구로 무시할 수 있으니까. @Valid를 붙이면 DTO의 @NotBlank, @Email 같은 규칙을 자동 검증.

프론트 코드 vs 백엔드 코드 비교

// ===== Next.js API Route (프론트엔드 개발자가 익숙한 코드) =====
// app/api/users/[id]/route.ts
export async function GET(req: Request, { params }: { params: { id: string } }) {
  const id = params.id;                          // URL에서 id 추출
  const user = await db.user.findUnique({ where: { id } });
  if (!user) return Response.json({ error: 'Not found' }, { status: 404 });
  return Response.json(user);
}

// ===== Spring Controller (위 코드와 완전히 같은 일을 한다) =====
@RestController                                   // = API 라우트 파일
@RequestMapping("/api/users")                     // = app/api/users/ 경로
public class UserController {
    @GetMapping("/{id}")                           // = [id]/route.ts의 GET
    public ResponseEntity<UserDTO> getUser(
        @PathVariable Long id                      // = params.id
    ) {
        UserDTO user = userService.findById(id);   // = db.user.findUnique
        return ResponseEntity.ok(user);            // = Response.json(user)
    }
}
5

JPA — DB 접근

SQL을 안 쓰고 함수 이름만으로 DB를 조회하는 마법

프론트에서 Prisma를 써봤다면 바로 이해된다. Prisma가 db.user.findMany()로 SQL 없이 DB를 조회하듯, Spring JPA도 함수 이름만 정의하면 SQL을 자동 생성한다.

🔮
JPA= “자동 번역기 (Java ↔ SQL)

Java 코드를 SQL로, SQL 결과를 Java 객체로 자동 번역해주는 기술. 프론트 개발자가 GraphQL 스키마를 정의하면 자동으로 쿼리가 만들어지는 것과 비슷하다. 직접 SQL을 쓰는 건 외국어로 대화하는 것, JPA는 통역사를 고용하는 것.

📐
Entity= “Prisma의 model (DB 테이블 설계도)

DB 테이블 1개 = Entity 클래스 1개. Prisma에서 model User { id Int @id, name String } 하듯, Java에서 @Entity class User { @Id Long id; String name; } 한다. 이 설계도를 보고 JPA가 테이블을 만들거나 매핑한다.

🪄
Repository 메서드 이름= “함수 이름이 곧 SQL

findByName('홍길동') → SELECT * FROM users WHERE name = '홍길동'. findByAgeGreaterThan(20) → WHERE age > 20. 영어 문장처럼 조합하면 Spring이 SQL로 자동 번역한다. Prisma의 where: { name: '홍길동' }과 같은 개념이지만, 함수 이름으로 표현한다.

함수 이름 → SQL 자동 생성 예시

// Repository 인터페이스 — 이름만 정의하면 끝!
@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    // 이름으로 찾기
    findByName("홍길동")
    → SELECT * FROM users WHERE name = '홍길동'

    // 이메일에 포함된 텍스트로 찾기 + 최신순 정렬
    findByEmailContainingOrderByCreatedAtDesc("gmail")
    → SELECT * FROM users WHERE email LIKE '%gmail%'
      ORDER BY created_at DESC

    // 나이 범위 + 활성 상태
    findByAgeBetweenAndActiveTrue(20, 30)
    → SELECT * FROM users WHERE age BETWEEN 20 AND 30
      AND active = true

    // 삭제
    deleteByName("홍길동")
    → DELETE FROM users WHERE name = '홍길동'

    // 개수 세기
    countByActive(true)
    → SELECT COUNT(*) FROM users WHERE active = true
}
Prisma (프론트)JS/TS ORM

db.user.findMany({ where: { age: { gt: 20 } } }) — 객체 문법으로 쿼리. Next.js와 궁합이 좋다.

Prisma → TypeScript → SQL

JPA (백엔드)Java ORM

findByAgeGreaterThan(20) — 함수 이름 문법으로 쿼리. Spring과 궁합이 좋다. 더 복잡한 건 @Query로 직접 작성.

JPA → Java → SQL

6

Spring Security

로그인, 권한 체크를 자동으로 해주는 보안 시스템

Next.js middleware.ts에서 로그인 체크하고 비로그인 유저를 /login으로 리다이렉트하는 것과 같은 역할을 한다. 다만 Spring Security는 훨씬 정교하다.

🏰
Security Filter Chain= “성(城)의 관문들

성에 들어가려면 여러 관문을 통과해야 한다. 1관문: '신분증 있어?' (인증) → 2관문: '여기 들어갈 자격 있어?' (인가) → 3관문: '위조 요청 아니지?' (CSRF). Next.js middleware는 관문 1개지만, Spring Security는 10개 넘는 관문이 자동으로 세팅된다.

🪪
인증 (Authentication)= “신분증 확인 — 너 누구야?

로그인과 같다. ID/비밀번호, JWT 토큰, OAuth 소셜 로그인 등으로 '이 사람이 누구인지' 확인한다. 프론트에서 localStorage에 토큰을 저장하고 매 요청에 Authorization 헤더로 보내면, Spring Security가 이 토큰을 검증한다.

🚫
인가 (Authorization)= “출입 권한 확인 — 너 여기 들어갈 수 있어?

로그인했다고 다 되는 건 아니다. 일반 유저는 /api/admin에 접근 못하고, 관리자만 가능. 프론트에서 role === 'admin'이면 관리자 메뉴를 보여주는 것과 같지만, 프론트는 속일 수 있으니 백엔드에서도 반드시 체크한다.

보안 설정

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) {
        http
            // REST API는 CSRF 비활성화 (SPA에서는 불필요)
            .csrf(csrf -> csrf.disable())

            // URL별 접근 권한 설정 (Next.js middleware의 matcher와 비슷)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()     // 누구나 접근 가능
                .requestMatchers("/api/admin/**").hasRole("ADMIN") // 관리자만
                .anyRequest().authenticated()                      // 나머지는 로그인 필요
            )

            // JWT 사용 (세션 없음 = SPA 친화적)
            .sessionManagement(s -> s.sessionCreationPolicy(STATELESS));

        return http.build();
    }
}
// 프론트에서 보내는 Authorization: Bearer <토큰>을
// Spring Security가 자동으로 검증하고 유저 정보를 꺼낸다
7

AOP

코드를 안 건드리고 기능을 끼워넣는 마법

React에서 HOC(Higher Order Component)커스텀 훅으로 로깅, 에러 바운더리, 권한 체크를 감싸는 것과 같은 패턴이다.

🧅
AOP (관점 지향 프로그래밍)= “양파 껍질 — 원래 코드를 감싸서 기능 추가

함수를 호출하면 '진짜 함수' 대신 '껍질을 두른 함수(프록시)'가 실행된다. 껍질이 '실행 전에 로그 남기기', '실행 후에 시간 측정', '에러 나면 Slack 알림' 같은 일을 한다. React에서 withAuth(MyComponent) 하면 로그인 체크가 자동 추가되는 것과 같다.

@Transactional= “AOP의 가장 흔한 사용 예

@Transactional을 Service 메서드에 붙이면, Spring이 프록시(껍질)를 만들어서 메서드 시작 전 BEGIN, 성공 시 COMMIT, 에러 시 ROLLBACK을 자동으로 한다. 개발자는 비즈니스 로직만 쓰면 되고, 트랜잭션 관리 코드를 안 써도 된다.

AOP 없이 (직접 관리)

public void createUser(UserDTO dto) {
  // 트랜잭션 시작 코드
  tx.begin();
  try {
    // 실제 비즈니스 로직
    repo.save(User.from(dto));
    // 성공 → 커밋
    tx.commit();
  } catch (Exception e) {
    // 실패 → 롤백
    tx.rollback();
    throw e;
  }
  // 매번 이걸 반복해야 한다...
}

AOP 사용 (@Transactional)

@Transactional  // ← 이 한 줄이면 끝!
public void createUser(UserDTO dto) {
  // 비즈니스 로직만 쓰면 된다
  repo.save(User.from(dto));

  // Spring AOP가 자동으로:
  // 시작 전 → BEGIN
  // 성공 시 → COMMIT
  // 에러 시 → ROLLBACK
}
8

한 장 요약

Spring 핵심 개념

01Spring = 식당 운영 시스템

주문(HTTP) → 주방장(Dispatcher) → 접수(Controller) → 요리사(Service) → 창고(Repository) → 냉장고(DB)

02DI = 콘센트에 플러그 꽂기

필요한 부품(Bean)을 직접 만들지 않고, Spring이 알아서 연결해준다. React Context와 비슷.

03Controller = page.tsx

URL과 매핑되는 진입점. 로직은 Service에 위임. 프론트 컴포넌트가 UI만 담당하는 것과 같다.

04Service = Custom Hook

비즈니스 로직의 집합. HTTP도 DB도 모르고, 순수한 업무 규칙만 담당한다.

05Repository = fetch wrapper

DB 접근을 추상화. 함수 이름만 정의하면 SQL이 자동 생성. Prisma와 비슷.

06@어노테이션 = 'use client'

코드 위에 붙이면 특별한 능력 부여. @RestController, @Service, @Transactional 등.

07Filter = middleware.ts

요청이 Controller에 도달하기 전 공통 처리. 인증, 로깅, CORS 등.

08AOP = HOC / 커스텀 훅

원래 코드를 감싸서 기능 추가. @Transactional이 대표적. withAuth(Component)와 같은 패턴.

Spring Framework