만약 여러분이 햄버거 매장의 점장이라고 상상해 봅시다. 카운터에서 손님의 주문(Request)을 받고 결제하는 직원이 있고, 뒤쪽 주방에서 패티를 굽고 햄버거를 조립(비즈니스 로직)하는 요리사가 있으며, 냉장고(Database)에서 신선한 식재료를 꺼내오는 창고 관리자가 있습니다. 이 세 명의 직원이 완벽하게 역할(Role)을 분담하여 일할 때 매장은 가장 효율적으로 돌아갑니다. 만약 카운터 직원이 주문을 받다 말고 주방에 들어가서 패티를 굽고 냉장고 문을 열어젖힌다면 매장은 금세 난장판이 될 것입니다. 스프링 부트(Spring Boot) 백엔드 애플리케이션도 이와 똑같습니다. 하나의 클래스 안에 모든 코드를 때려 넣는 '스파게티 코드'를 방지하고, 유지보수와 테스트를 극대화하기 위해 탄생한 아키텍처가 바로 MVC(Model-View-Controller) 패턴과 3계층 아키텍처(3-Tier Architecture)입니다.
이번 시리즈에서는 "백엔드 개발의 알파이자 오메가"인 Controller, Service, Repository의 명확한 역할 분담과 실무 규칙에 대하여 다뤄보겠습니다.

💁♂️ 1. Controller (표현 계층 - 카운터 직원)
Controller(컨트롤러)는 매장의 카운터 직원과 같습니다. 클라이언트(브라우저, 모바일 앱)로부터 들어오는 HTTP 요청(GET, POST 등)을 가장 먼저 맞이하는 관문(Entry Point)입니다.

- 주요 역할:
- 사용자의 요청(Request) URL과 HTTP Method를 매핑(
@GetMapping,@PostMapping) - 사용자가 보낸 데이터(JSON 파라미터, 쿼리 스트링)의 형식이 올바른지 유효성 검증(Validation)
- (핵심) 주방장(Service)에게 요리 지시를 내림
- 요리가 완성되면 손님에게 응답(Response, JSON 또는 HTML) 반환
- 사용자의 요청(Request) URL과 HTTP Method를 매핑(
- 절대 금기사항: 컨트롤러 안에서 비밀번호를 암호화하거나, 데이터를 계산하는 등 비즈니스 로직(Business Logic)을 직접 짜면 절대 안 됩니다. 카운터 직원이 패티를 구우면 안 되는 것과 같습니다. 무조건 Service 클래스로 넘겨야 합니다.
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
// 카운터 직원은 주방장(Service)의 연락처만 알고 있습니다.
private final UserService userService;
@PostMapping("/signup")
public ResponseEntity<String> signup(@RequestBody @Valid UserSignupDto dto) {
// 컨트롤러는 요리(로직)를 하지 않고, Service에게 데이터(DTO)만 툭 던지며 지시합니다.
userService.registerUser(dto);
return ResponseEntity.ok("회원가입 성공!"); // 그리고 결과를 손님에게 반환합니다.
}
}
🧑🍳 2. Service (비즈니스 계층 - 주방장)
Service(서비스)는 매장의 주방장입니다. 애플리케이션의 가장 핵심적인 뇌(Brain)이자 비즈니스 로직이 살아 숨 쉬는 곳입니다. 트랜잭션(Transaction)이 시작되고 끝나는 곳이기도 합니다.
- 주요 역할:
- 컨트롤러가 넘겨준 데이터를 바탕으로 실질적인 계산, 검증, 상태 변경 등의 핵심 비즈니스 로직 수행
- "이미 가입된 이메일인지 검사하라", "비밀번호를 암호화하라", "회원 등급을 올려라" 등등
- (핵심) 데이터가 필요하면 창고 관리자(Repository)에게 식재료(Data)를 요청함
- 실무 팁: 서비스 코드는 최대한 스프링 프레임워크나 HTTP 기술에 종속되지 않게 순수한 자바(Java) 코드로 유지하는 것이 테스트하기 가장 좋습니다. 서비스 계층에
HttpServletRequest같은 웹 계층 객체가 파라미터로 넘어오면 설계가 꼬인 것입니다.
@Service
@RequiredArgsConstructor
public class UserService {
// 주방장은 창고 관리자(Repository)의 연락처를 알고 있습니다.
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder; // 암호화 도구
@Transactional
public void registerUser(UserSignupDto dto) {
// 비즈니스 로직 1: 이메일 중복 검사
if (userRepository.existsByEmail(dto.getEmail())) {
throw new IllegalArgumentException("이미 존재하는 이메일입니다.");
}
// 비즈니스 로직 2: 비밀번호 암호화 및 유저 객체 생성
String encodedPassword = passwordEncoder.encode(dto.getPassword());
UserEntity newUser = UserEntity.builder()
.email(dto.getEmail())
.password(encodedPassword)
.build();
// 창고 관리자에게 저장을 부탁함
userRepository.save(newUser);
}
}
🗄️ 3. Repository (데이터 접근 계층 - 창고 관리자)
Repository(리포지토리)는 냉장고와 창고(Database)를 관리하는 직원입니다. 데이터를 저장(Insert)하고, 찾고(Select), 수정하고(Update), 삭제하는(Delete) 순수한 데이터베이스 통신 역할만 전담합니다.
- 주요 역할: 데이터베이스(MySQL, Oracle 등)와 직접 연결되어 쿼리(Query)를 수행합니다.
- 특징: 과거에는 SQL 쿼리를 길게 직접 짰지만, 현대의 Spring Data JPA를 사용하면 인터페이스(Interface)만 선언해 둬도 스프링이 알아서 창고 관리자 객체를 만들어 줍니다.
// JpaRepository를 상속받는 것만으로 기본 CRUD(저장, 조회, 삭제) 기능이 완성됩니다.
public interface UserRepository extends JpaRepository<UserEntity, Long> {
// 메서드 이름만 잘 지어도 JPA가 알아서 SQL(SELECT count(*) FROM users WHERE email=?)로 번역해 줍니다!
boolean existsByEmail(String email);
}
🎯 4. 마무리 및 다음 단계
지금까지 복잡한 웹 애플리케이션의 코드를 3등분하여, 카운터 직원(Controller)이 요청을 받고, 주방장(Service)이 핵심 로직을 처리하며, 창고 관리자(Repository)가 DB와 소통하는 '3-Tier 아키텍처의 완벽한 분업화'에 대해 밀도 있게 다루어 보았습니다. 이 원칙만 철저히 지켜도 여러분의 코드는 스파게티가 되지 않고 깔끔하게 유지보수될 수 있습니다.
코드의 구조는 완벽해졌습니다. 그런데 프로젝트를 진행하다 보니 데이터베이스 접속 정보, 포트 번호, 외부 API 키 등 '설정값'들을 저장해야 할 일이 생겼습니다. 이 설정값들을 자바 코드 안에 하드코딩(Hard-coding)하면 로컬 개발 환경과 실제 상용(Production) 서버 환경이 달라서 빌드할 때마다 코드를 고쳐야 하는 끔찍한 사태가 발생합니다. 이어지는 04단계 포스팅에서는 "로컬 서버와 상용 서버의 환경을 단 한 줄의 코드로 스위칭(Switching)한다!" application.yml 파일의 마법과 프로필(Profile)을 활용한 완벽한 환경 분리 실무 전략에 대해 아주 치밀하게 파헤쳐 보겠습니다!
'Framework > Spring Boot' 카테고리의 다른 글
| [Spring Boot] 의존성 주입(DI)과 IoC 컨테이너: 객체의 생과 사를 스프링에게 맡기다 (0) | 2026.07.23 |
|---|---|
| [Spring Boot] Logback 완벽 가이드: 서버의 유일한 목격자, 로깅(Logging) 마스터하기 (0) | 2026.07.23 |
| [Spring Boot] application.yml 완벽 가이드: Profile을 활용한 무결점 환경 분리 전략 (0) | 2026.07.23 |
| [Spring Boot] Spring Initializr: 완벽한 프로젝트 뼈대와 실무 의존성 설계 가이드 (0) | 2026.07.23 |
| [Spring Boot] 3.x 시작하기: 레거시 Spring과의 차이점과 완벽한 첫 세팅 (0) | 2026.07.23 |