Spring Boot를 처음 배우다 보면 코드가 점점 길어지기 시작합니다.

처음에는 컨트롤러 하나에 모든 코드를 넣어도 동작은 합니다.

예를 들어:

  • 요청 받기
  • 값 확인하기
  • 회원 저장하기
  • 결과 반환하기

이 모든 일을 한 파일에 넣을 수도 있습니다.

하지만 프로젝트가 조금만 커져도 코드가 금방 복잡해집니다.

그래서 Spring Boot에서는 보통 역할을 나눕니다.

이번 글에서는 가장 기본이 되는 세 가지 역할을 정리해보겠습니다.

  • Controller
  • Service
  • Repository

이번 글의 목표는 딱 하나입니다.

코드를 왜 나눠서 쓰는지 이해하기


먼저 결론부터 보기

Spring Boot에서 자주 쓰는 구조는 대략 이렇습니다.

브라우저 또는 클라이언트
        ↓
Controller
        ↓
Service
        ↓
Repository
        ↓
Database

각 역할을 아주 간단히 말하면 다음과 같습니다.

계층역할
Controller요청을 받는 곳
Service핵심 로직을 처리하는 곳
Repository데이터를 저장하거나 조회하는 곳

처음에는 이렇게 기억하면 됩니다.

Controller = 입구
Service = 판단하는 곳
Repository = 데이터 창고와 연결하는 곳

왜 코드를 나눠야 할까?

처음에는 이런 생각이 들 수 있습니다.

"그냥 Controller에 다 쓰면 안 되나?"

가능은 합니다.

하지만 좋은 방식은 아닙니다.

왜냐하면 Controller에 모든 코드를 넣으면 Controller가 너무 많은 일을 하게 됩니다.

예를 들어 회원 가입 기능을 생각해보겠습니다.

회원 가입에는 이런 일이 필요할 수 있습니다.

  • 사용자가 보낸 이름을 받기
  • 이름이 비어 있는지 확인하기
  • 이미 가입한 회원인지 확인하기
  • 회원 정보를 저장하기
  • 결과를 응답하기

이걸 전부 Controller에 넣으면 코드가 길어지고, 나중에 수정하기 어려워집니다.

그래서 역할을 나눕니다.

요청 받기 → Controller
가입 규칙 판단 → Service
회원 저장 → Repository

이렇게 나누면 코드가 훨씬 읽기 쉬워집니다.


비유로 이해하기

식당으로 비유하면 이해하기 쉽습니다.

손님이 식당에 와서 음식을 주문합니다.

이때 역할은 이렇게 나눌 수 있습니다.

식당Spring Boot
손님 주문을 받는 직원Controller
주문을 보고 조리 방법을 결정하는 주방Service
재료 창고에서 재료를 꺼내는 곳Repository

손님 주문을 받는 직원이 직접 요리하고, 재료 관리까지 전부 하면 너무 복잡합니다.

그래서 역할을 나눕니다.

Spring Boot도 마찬가지입니다.


Controller란?

Controller는 요청을 받는 곳입니다.

브라우저나 클라이언트가 서버에 요청을 보내면 가장 먼저 Controller가 받습니다.

예를 들어 사용자가 아래 주소로 요청을 보낸다고 해보겠습니다.

http://localhost:8080/members

그러면 Controller는 이 요청을 받고 어떤 기능을 실행할지 결정합니다.

간단한 예시는 다음과 같습니다.

@Controller
public class MemberController {

    @GetMapping("/members")
    @ResponseBody
    public String members() {
        return "member list";
    }
}

여기서 Controller는 /members 요청을 받아서 응답을 돌려줍니다.

처음에는 이렇게 이해하면 됩니다.

Controller는 외부 요청이 들어오는 입구입니다.


Controller가 하면 좋은 일

Controller는 이런 일을 담당하는 것이 좋습니다.

  • URL 요청 받기
  • 요청 값 받기
  • Service 호출하기
  • 응답 반환하기

즉, Controller는 요청과 응답에 집중해야 합니다.

반대로 Controller가 너무 많은 판단을 직접 하면 코드가 복잡해집니다.

예를 들어 이런 로직은 Controller에 많이 넣지 않는 것이 좋습니다.

  • 회원 이름 중복 검사
  • 할인 금액 계산
  • 주문 가능 여부 판단
  • 데이터 저장 규칙

이런 핵심 로직은 보통 Service로 보냅니다.


Service란?

Service는 비즈니스 로직을 처리하는 곳입니다.

비즈니스 로직이라는 말이 처음에는 어렵게 느껴질 수 있습니다.

쉽게 말하면, 서비스의 핵심 규칙입니다.

예를 들어 회원 가입 기능에서는 이런 규칙이 있을 수 있습니다.

  • 이름이 비어 있으면 가입 불가
  • 이미 같은 이름의 회원이 있으면 가입 불가
  • 조건을 통과하면 회원 저장

이런 판단은 단순히 요청을 받는 일이 아닙니다.

서비스의 규칙을 처리하는 일입니다.

그래서 Service에 둡니다.

예시는 다음과 같습니다.

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public void join(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("이름은 비어 있을 수 없습니다.");
        }

        memberRepository.save(name);
    }
}

여기서 Service는 이름이 비어 있는지 확인하고, 문제가 없으면 Repository에 저장을 맡깁니다.

처음에는 이렇게 이해하면 됩니다.

Service는 실제 규칙과 판단을 처리하는 곳입니다.


Repository란?

Repository는 데이터를 저장하거나 조회하는 곳입니다.

처음에는 DB를 직접 연결하지 않고, 메모리에 저장한다고 생각해도 됩니다.

예를 들어 회원 이름을 저장하는 Repository를 간단히 만들면 다음과 같습니다.

@Repository
public class MemberRepository {

    private final List<String> members = new ArrayList<>();

    public void save(String name) {
        members.add(name);
    }

    public List<String> findAll() {
        return members;
    }
}

Repository는 데이터를 다루는 역할을 합니다.

나중에 DB를 연결하면 Repository는 실제 데이터베이스와 연결되는 역할을 하게 됩니다.

처음에는 이렇게 이해하면 됩니다.

Repository는 데이터 저장소와 대화하는 곳입니다.


전체 예시로 보기

회원 이름을 받아서 저장하는 아주 단순한 예시를 생각해보겠습니다.

흐름은 이렇습니다.

브라우저 요청
        ↓
Controller가 name 값을 받음
        ↓
Service가 가입 규칙 확인
        ↓
Repository가 이름 저장
        ↓
Controller가 결과 응답

Controller 예시

@Controller
public class MemberController {

    private final MemberService memberService;

    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    @GetMapping("/join")
    @ResponseBody
    public String join(@RequestParam("name") String name) {
        memberService.join(name);
        return "saved: " + name;
    }
}

Controller는 name 값을 받고 Service를 호출합니다.

여기서 Controller는 직접 저장하지 않습니다.

저장은 Service를 통해 진행합니다.


Service 예시

@Service
public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public void join(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("이름은 비어 있을 수 없습니다.");
        }

        memberRepository.save(name);
    }
}

Service는 이름이 비어 있는지 확인합니다.

그리고 문제가 없으면 Repository에 저장을 맡깁니다.


Repository 예시

@Repository
public class MemberRepository {

    private final List<String> members = new ArrayList<>();

    public void save(String name) {
        members.add(name);
    }

    public List<String> findAll() {
        return members;
    }
}

Repository는 데이터를 저장하고 조회합니다.

지금은 메모리에 저장하지만, 나중에는 이 부분이 DB 연결 코드로 바뀔 수 있습니다.


세 파일의 역할 다시 정리

같은 기능을 세 계층으로 나누면 이렇게 됩니다.

파일역할
MemberController요청을 받고 응답을 반환
MemberService회원 가입 규칙 처리
MemberRepository회원 데이터 저장

즉, 각 파일이 자기 역할에 집중합니다.


이렇게 나누면 뭐가 좋을까?

1. 코드 읽기가 쉬워진다

Controller를 보면 요청 흐름을 알 수 있습니다.

Service를 보면 핵심 규칙을 알 수 있습니다.

Repository를 보면 데이터 저장 방식을 알 수 있습니다.

즉, 코드를 찾기 쉬워집니다.


2. 수정하기 쉬워진다

예를 들어 저장 방식을 메모리에서 DB로 바꾼다고 해보겠습니다.

역할이 잘 나뉘어 있다면 Repository 쪽만 주로 수정하면 됩니다.

Controller와 Service를 크게 건드리지 않아도 됩니다.


3. 테스트하기 쉬워진다

Service에 핵심 로직이 모여 있으면 테스트하기 쉬워집니다.

예를 들어 회원 이름이 비어 있을 때 에러가 나는지 테스트할 수 있습니다.

Controller에 모든 코드가 섞여 있으면 이런 테스트가 더 어려워집니다.


초보자가 헷갈리기 쉬운 부분

1. Controller에 다 넣으면 안 되나?

처음에는 넣어도 동작은 합니다.

하지만 기능이 늘어나면 Controller가 너무 복잡해집니다.

그래서 처음부터 역할을 나누는 습관을 들이는 것이 좋습니다.


2. Service는 꼭 필요한가?

아주 작은 예제에서는 Service가 없어도 됩니다.

하지만 실제 프로젝트에서는 규칙과 판단이 생깁니다.

그때 Service가 없으면 Controller나 Repository에 로직이 섞이게 됩니다.

그래서 Service를 따로 두는 것이 좋습니다.


3. Repository는 DB가 있을 때만 쓰나?

꼭 그렇지는 않습니다.

처음에는 메모리에 저장해도 Repository라고 볼 수 있습니다.

중요한 것은 데이터를 다루는 역할을 분리하는 것입니다.

나중에 DB를 연결하면 Repository가 DB와 대화하는 역할을 하게 됩니다.


요청 흐름 그림으로 보기

사용자
  ↓
브라우저
  ↓
Controller
  ↓
Service
  ↓
Repository
  ↓
데이터 저장소

반대로 응답은 다시 위로 올라옵니다.

데이터 저장소
  ↓
Repository
  ↓
Service
  ↓
Controller
  ↓
브라우저
  ↓
사용자

이 흐름을 알면 Spring Boot 코드 구조가 훨씬 덜 낯설어집니다.


오늘 배운 것

이번 글에서는 Controller, Service, Repository를 나누는 이유를 정리했습니다.

핵심은 아래와 같습니다.

  • Controller는 요청을 받는 곳
  • Service는 핵심 로직을 처리하는 곳
  • Repository는 데이터를 저장하거나 조회하는 곳
  • 코드를 나누면 읽기 쉽고 수정하기 쉬워짐
  • 처음부터 역할을 나누는 습관이 중요함

정리

Spring Boot에서 코드를 나누는 이유는 어렵게 보이려고 하는 것이 아닙니다.

오히려 코드를 더 단순하게 관리하기 위해서입니다.

처음에는 세 계층이 많아 보일 수 있습니다.

하지만 역할로 보면 단순합니다.

Controller = 요청 받기
Service = 규칙 처리하기
Repository = 데이터 다루기

이 흐름을 이해하면 앞으로 회원 등록, 조회, 수정, 삭제 같은 기능을 만들 때 코드가 훨씬 정리됩니다.

다음 글에서는 간단한 회원 등록 예제를 통해 이 구조가 실제로 어떻게 연결되는지 정리해보겠습니다.