한 클래스가 웹 처리 + 로직 + DB를 전부 다 하면 금방 뒤엉켜요. 스프링 백엔드는 역할별로 계층을 나눠 관심사를 분리합니다. 이번 장에서 그 3계층 구조를 코드로 잡아봐요.
🎯 이 장을 끝내면
왜 계층을 나누는지(관심사 분리)를 설명할 수 있어요.
Controller · Service · Repository 3계층의 역할을 구분해요.
요청이 Controller → Service → Repository → DB로 흐른다는 걸 알아요.
계층끼리 DI(주입)로 연결된다는 걸 코드로 이해해요.
😵
왜 나누나
한 클래스가 다 하면 왜 힘들까
문제를 알면 "계층"이라는 해법이 자연스럽게 이해돼요.
🔤
계층구조(Layered Architecture) — 코드를 역할별 "층(layer)"으로 나눠 쌓는 설계예요. 위층은 바로 아래층에만 일을 맡기고, 각 층은 자기 역할에만 집중해요.
하나의 클래스가 웹 요청 처리 + 비즈니스 로직 + 데이터베이스 접근을 한꺼번에 담당한다고 해봐요. 처음엔 편해 보이지만, 기능이 늘수록 코드가 복잡해지고, 중복이 생기고, 테스트하기 어려워져요. 웹 코드와 DB 코드가 뒤섞여 있어서, 로직 하나만 바꾸려 해도 전부 얽혀 있거든요.
🔤
비즈니스 로직(business logic) — 앱이 진짜로 처리하려는 업무 규칙과 계산이에요. 예를 들어 주문 금액을 계산하거나 회원 자격을 검증하는 것처럼, "그 서비스가 실제로 해야 하는 일"을 말해요.
🍝 모든 걸 한 곳에서 — "스파게티" 코드
웹 파라미터를 받는 코드, 계산·검증 로직, SQL 쿼리가 한 메서드 안에 뒤엉켜 있으면, 무엇이 무엇을 하는지 알기 어렵고 재사용·테스트가 거의 불가능해요.
해법은 간단해요. 역할별로 계층을 나눠 관심사를 분리하는 거예요. 각 계층은 자기 일만 하고, 옆 계층에 나머지를 맡깁니다.
🔤
관심사 분리(Separation of Concerns) — 성격이 다른 일(웹 처리·로직·DB)을 한데 섞지 않고 각각 다른 계층에 맡기는 설계 원칙이에요. 각 계층이 자기 "관심사(역할)"에만 집중하게 만들어요.
ControllerServiceRepository3계층관심사 분리DIMVC
🔤
MVC (Model-View-Controller) — 화면(View)·데이터(Model)·요청 처리(Controller)로 역할을 나누는 대표적인 설계 패턴이에요. 이번 장에서 배울 Controller·Service·Repository 3계층도 "역할을 나눠 관심사를 분리한다"는 같은 정신에서 나온 구조랍니다.
🧭
이번 장의 큰 그림. "요청이 들어오면 어느 층이 무엇을 하는가"만 잡으면 돼요. 세 계층의 역할 구분과 흐름 방향, 이 두 가지가 핵심입니다.
🖼️ 그림으로 보기 — 요청이 흐르는 3계층
요청은 Controller → Service → Repository → DB 순으로 내려가고, 결과(응답)는 그 역순으로 올라와요. 각 층은 자기 일만 하고 옆 층에 나머지를 맡겨요(관심사 분리). Controller가 DB를 직접 건드리지 않는 게 핵심이에요.
🏛️
3계층
Controller · Service · Repository
스프링 백엔드의 대표적인 3계층 구조예요.
스프링 애플리케이션은 보통 세 개의 계층으로 역할을 나눠요. 각 계층은 딱 자기 책임만 갖고, 필요한 나머지는 아래 계층에 위임합니다.
🍽️
식당으로 비유하면.Controller는 손님의 주문을 받고 음식을 내어주는 홀 직원, Service는 주문대로 요리하는 주방장, Repository는 재료 창고를 관리하는 사람이에요. 홀 직원이 직접 창고에 들어가지 않고 주방장에게 맡기듯, Controller도 DB(창고)를 직접 건드리지 않고 반드시 Service를 거쳐요.
🔤
CRUD — 데이터를 다루는 네 가지 기본 동작인 생성(Create)·조회(Read)·수정(Update)·삭제(Delete)의 앞글자를 딴 말이에요. Repository가 담당하는 "저장·조회·수정·삭제"가 바로 이 CRUD랍니다.
계층
어노테이션
역할
Controller (컨트롤러)
@Controller
웹 진입점. 클라이언트의 요청을 받고, 결과를 응답으로 돌려줘요. 로직은 직접 하지 않고 Service에 맡겨요.
Service (서비스)
@Service
비즈니스 로직·규칙을 처리해요. 계산·검증·조합 등 "진짜 일". 트랜잭션 경계가 보통 여기예요.
Repository (리포지토리)
@Repository
DB 접근 담당. 데이터를 저장·조회·수정·삭제해요. 데이터베이스와 대화하는 유일한 층.
🧱 그림으로 보기 — 위에서 아래로 쌓인 3계층
🚪
Controller @Controller
요청을 받고 응답을 돌려주는 웹 진입점
↓ 아래층에 위임
🧠
Service @Service
비즈니스 로직·규칙 처리, 트랜잭션 경계
↓ 아래층에 위임
🗄️
Repository @Repository
DB 접근 — 저장·조회·수정·삭제
↓
💾
Database
실제 데이터가 저장되는 곳
위에서 아래로 한 방향으로만 일을 맡겨요. 위층은 바로 아래층만 사용하고, 아래층은 위층을 알지 못해요(단방향 의존).
🔤
트랜잭션(transaction) — 여러 DB 작업을 "모두 성공 아니면 모두 취소"로 묶는 하나의 단위예요. 중간에 하나라도 실패하면 앞선 작업까지 되돌려(rollback) 데이터가 어긋나지 않게 해요. 보통 Service 계층이 이 경계를 담당해요.
💡
한 줄 요약.Controller는 "받고 돌려주는 문지기", Service는 "일하는 두뇌", Repository는 "DB 창고지기"예요. 역할이 겹치지 않게 나누는 게 핵심이에요.
➡️
요청 흐름
요청은 어떻게 층을 타고 흐를까
들어갈 땐 위 → 아래, 나올 땐 아래 → 위(역순)예요.
클라이언트가 요청을 보내면, Controller → Service → Repository → DB 순서로 내려가요. 그리고 결과(응답)는 그 역순으로 다시 올라와 클라이언트에게 돌아갑니다.
🌐
클라이언트
요청 보냄
→
🚪
Controller
요청 받기
→
🧠
Service
로직 처리
→
🗄️
Repository
DB 접근
🔁 응답은 역순으로
Repository가 DB에서 데이터를 가져오면 → Service가 그것으로 로직을 마무리하고 → Controller가 결과를 받아 응답으로 클라이언트에게 돌려줘요.
즉 Controller → Service → Repository → DB로 내려갔다가, 그 결과가 역방향으로 올라와 클라이언트에게 도착해요. 방향과 순서를 꼭 기억하세요.
📌
Controller가 Repository를 직접 호출하지 않는 게 원칙이에요. 반드시 Service를 거쳐 내려가요. 그래야 비즈니스 규칙·트랜잭션이 Service 한 곳에 모여요.
한 계층이 아래 계층을 쓰려면, 그 객체를 직접 new 하지 않고 스프링에게서 주입받아(DI) 사용해요. Controller는 Service를, Service는 Repository를 생성자로 주입받습니다. (CH 02의 DI가 여기서 그대로 쓰여요.)
🗄️ Repository — DB 접근 계층
데이터를 저장·조회하는 층이에요. @Repository로 스프링 빈이 돼요.
@Repository
public class UserRepository {
public User findById(Long id) {
// DB에서 사용자 조회 (여기선 예시)
return new User(id, "태호");
}
}
🧠 Service — 비즈니스 로직 계층
Repository를 생성자로 주입받아 로직을 처리해요. @Service로 빈이 됩니다.
@Service
public class UserService {
private final UserRepository userRepository;
// Repository를 주입받음 (DI)
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUser(Long id) {
// 비즈니스 로직 처리 후 Repository 사용
return userRepository.findById(id);
}
}
🚪 Controller — 웹 진입점 계층
요청을 받아 Service를 주입받아 호출하고, 결과를 응답으로 돌려줘요. @Controller로 빈이 됩니다.
@Controller
public class UserController {
private final UserService userService;
// Service를 주입받음 (DI)
public UserController(UserService userService) {
this.userService = userService;
}
public User user(Long id) {
// 요청을 받아 Service에 위임하고 응답
return userService.getUser(id);
}
}
💉
세 코드의 공통점: 필요한 아래 계층을 final 필드로 두고, 생성자로 주입받아요. new UserRepository() 같은 코드가 보이지 않죠? 그게 바로 스프링이 대신 연결해주는 DI예요.
✨
관심사 분리
나누면 무엇이 좋아질까
관심사 분리(Separation of Concerns)의 실제 이점이에요.
이점
왜 좋아지나
유지보수
고칠 곳이 명확해요. 로직은 Service, DB는 Repository만 보면 되니 수정 범위가 좁아져요.
테스트
계층이 분리돼 있어 각 계층을 따로 테스트할 수 있어요. Service만 떼어내 로직만 검증 가능.
재사용
같은 Service를 여러 Controller가 쓰거나, 같은 Repository를 여러 Service가 공유할 수 있어요.
🔧
유지보수 쉬움
고칠 곳이 명확해요. 로직은 Service, DB는 Repository만 보면 돼요.
🧪
테스트 쉬움
계층이 나뉘어 있어 Service만 떼어내 로직만 따로 검증할 수 있어요.
♻️
재사용 쉬움
같은 Service·Repository를 여러 상위 계층이 함께 나눠 쓸 수 있어요.
🏛️
이렇게 역할을 층으로 나누는 것이 바로 관심사 분리예요. 각 계층은 자기 일에만 집중하고, 나머지는 옆 계층에 맡기죠. 코드가 커져도 깔끔하게 유지되는 비결이에요.
🧠 이 장 핵심 요약
한 클래스가 웹·로직·DB를 다 하면 복잡·중복·테스트 곤란 → 역할별로 계층을 나눠 관심사를 분리해요.