본문 바로가기
Framework/Spring Boot

[Spring Boot] Spring Boot MVC 패턴 완벽 해부: Controller, Service, Repository의 분업

반응형

만약 여러분이 햄버거 매장의 점장이라고 상상해 봅시다. 카운터에서 손님의 주문(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) 반환
  • 절대 금기사항: 컨트롤러 안에서 비밀번호를 암호화하거나, 데이터를 계산하는 등 비즈니스 로직(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)을 활용한 완벽한 환경 분리 실무 전략에 대해 아주 치밀하게 파헤쳐 보겠습니다!

반응형