티스토리 뷰

300x250

Spring Boot를 처음 공부할 때 많은 사람이 가장 헷갈려하는 부분 중 하나가 바로 Controller, Service, Repository를 왜 나누는가입니다. 코드를 따라 치다 보면 클래스는 늘어나는데, 각 계층이 무엇을 맡고 어디까지 책임져야 하는지는 오히려 더 흐릿하게 느껴질 수 있습니다.

특히 입문 단계에서는 Controller에 로직을 다 넣어도 일단 동작은 하기 때문에, 굳이 ServiceRepository를 나눠야 하는 이유가 잘 와닿지 않는 경우가 많습니다. 하지만 프로젝트가 조금만 커져도 이 구조를 이해했는지 여부가 유지보수성과 가독성을 크게 갈라놓습니다.

결론부터 말하면 Spring MVC 구조의 핵심은 요청을 받는 역할, 비즈니스 로직을 처리하는 역할, DB에 접근하는 역할을 분리하는 것입니다. 이 글에서는 Controller, Service, Repository의 역할과 요청이 실제로 어떻게 흐르는지 개념부터 코드 예제까지 한 번에 정리하겠습니다.

  • Controller, Service, Repository는 각각 무엇을 할까?
  • 요청 하나가 들어오면 실제로 어떤 순서로 흐를까?
  • 어디까지 Controller에 두고, 어디부터 Service로 넘겨야 할까?
  • 초보자가 이 구조에서 자주 하는 실수는 무엇일까?
핵심 요약
Controller는 HTTP 요청을 받고 응답 형태를 결정하는 역할입니다.
Service는 실제 비즈니스 로직을 처리하는 중심 계층입니다.
Repository는 DB 조회·저장 같은 데이터 접근을 담당합니다.
즉 Spring MVC 구조는 요청 처리 책임을 역할별로 분리해 코드 흐름을 명확하게 만드는 방식입니다.
728x90
쉬운 정의 | Spring MVC 구조를 한 문장으로 잡아보자
클래스를 나누는 이유를 먼저 이해하면 전체 흐름이 훨씬 쉬워집니다.

Spring MVC 구조는 요청을 받는 부분, 처리하는 부분, 저장하는 부분을 나눠서 관리하는 구조라고 보면 가장 이해하기 쉽습니다.

예를 들어 회원 목록을 조회하는 기능이 있다고 가정해보겠습니다. 사용자가 브라우저나 프론트엔드에서 요청을 보내면, 이 요청을 먼저 받는 곳은 Controller입니다. 하지만 Controller가 데이터 조회, 조건 검사, 비즈니스 규칙 처리, DB 접근까지 모두 맡아버리면 코드가 빠르게 복잡해집니다.

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

  • Controller: 요청을 받음
  • Service: 로직을 처리함
  • Repository: 데이터를 조회·저장함
Spring MVC 구조는 “누가 요청을 받고, 누가 처리하고, 누가 저장하는가”를 분리하는 구조라고 이해하면 됩니다.
왜 중요한가 | Controller에 다 넣으면 어떤 문제가 생길까?
구조 분리를 왜 하는지 이해하면 계층 설계가 훨씬 자연스러워집니다.

처음에는 Controller 하나에 코드를 몰아넣어도 기능은 돌아갈 수 있습니다. 하지만 기능이 늘어나면 아래 문제가 바로 생깁니다.

  • 요청 처리 코드와 비즈니스 로직이 섞여서 읽기 어려워짐
  • 같은 로직을 여러 API에서 재사용하기 어려워짐
  • 테스트 코드 작성이 불편해짐
  • DB 접근 코드까지 Controller에 들어오면서 책임이 과해짐
  • 수정 범위가 커져 유지보수가 어려워짐

즉 구조를 나누는 이유는 “폼”이 아니라 코드 책임을 분리해서 읽기 쉽고 수정하기 쉽게 만들기 위해서입니다.

초보자가 자주 하는 오해
Controller, Service, Repository를 나누는 이유는 단순히 파일 수를 늘리기 위해서가 아닙니다. 같은 기능이라도 책임을 분리해야 코드가 커졌을 때 버틸 수 있기 때문입니다.
기본 설명 | Controller, Service, Repository 역할을 구분해보자
각 계층의 책임을 분명히 잡아야 구조가 선명해집니다.
계층 역할 핵심 포인트
Controller 요청 수신, 파라미터 처리, 응답 반환 비즈니스 로직을 길게 넣기보다 Service 호출에 집중하는 편이 좋습니다.
Service 비즈니스 규칙 처리, 흐름 제어 핵심 로직을 모아두는 중심 계층입니다.
Repository DB 저장, 조회, 수정, 삭제 데이터 접근에 집중하고 비즈니스 판단은 Service 쪽에 두는 편이 좋습니다.
요청 흐름을 한 줄로 보면
클라이언트가 요청을 보냄
Controller가 요청을 받음
Service가 필요한 로직을 처리함
Repository가 DB와 통신하고 결과를 반환함
소스코드 예제 | 회원 목록 조회 요청은 이렇게 흐릅니다
개념 설명 다음에는 실제 코드 흐름으로 보는 편이 훨씬 이해가 쉽습니다.

먼저 Controller는 요청을 받고 Service를 호출합니다.

@RestController
@RequestMapping("/members")
public class MemberController {

    private final MemberService memberService;

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

    @GetMapping
    public List<MemberResponse> getMembers() {
        return memberService.getMembers();
    }
}

이 코드에서 Controller는 HTTP 요청을 받아서 memberService.getMembers()를 호출하고 결과를 반환하는 역할에 집중합니다.

그 다음 Service는 실제 비즈니스 흐름을 처리합니다.

@Service
public class MemberService {

    private final MemberRepository memberRepository;

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

    public List<MemberResponse> getMembers() {
        return memberRepository.findAll()
                .stream()
                .map(MemberResponse::new)
                .toList();
    }
}

Service는 직접 HTTP 요청을 받지 않고, Repository에서 데이터를 가져와 필요한 형태로 가공하는 역할을 맡습니다.

마지막으로 Repository는 DB 접근에 집중합니다.

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
}

즉 이 흐름은 아래처럼 이해하면 됩니다.

GET /members 요청
   ↓
MemberController#getMembers()
   ↓
MemberService#getMembers()
   ↓
MemberRepository#findAll()
   ↓
DB 조회 결과 반환
비교/구조 | Controller, Service, Repository에 무엇을 두어야 할까?
책임 경계를 잘 잡아야 계층 분리가 무너지지 않습니다.
구분 넣어도 되는 것 가급적 피할 것
Controller 요청 매핑, 파라미터 처리, 응답 반환 복잡한 계산, 긴 조건문, DB 직접 접근
Service 비즈니스 규칙, 트랜잭션, 여러 Repository 조합 HTTP 세부 처리, 뷰 렌더링 책임
Repository 조회/저장/수정/삭제 쿼리 비즈니스 판단, 요청/응답 포맷 처리
소스코드 예제 | Controller에 로직을 몰아넣으면 이렇게 됩니다
좋지 않은 예시를 보면 왜 계층 분리가 필요한지 더 빨리 이해됩니다.
@RestController
@RequestMapping("/members")
public class MemberController {

    private final MemberRepository memberRepository;

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

    @PostMapping
    public String saveMember(@RequestBody MemberRequest request) {
        if (request.getName() == null || request.getName().isBlank()) {
            return "이름이 필요합니다.";
        }

        Member member = new Member(request.getName(), request.getAge());
        memberRepository.save(member);

        return "저장 완료";
    }
}

이 코드는 동작은 할 수 있지만, 검증 로직과 저장 로직이 모두 Controller 안에 들어가 있습니다. 기능이 조금만 더 늘어나도 Controller가 비대해지고 재사용도 어려워집니다.

이를 Service로 분리하면 이런 형태가 됩니다.

@Service
public class MemberService {

    private final MemberRepository memberRepository;

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

    public void saveMember(MemberRequest request) {
        if (request.getName() == null || request.getName().isBlank()) {
            throw new IllegalArgumentException("이름이 필요합니다.");
        }

        Member member = new Member(request.getName(), request.getAge());
        memberRepository.save(member);
    }
}

즉 Controller는 요청을 받고 Service에 위임하고, 핵심 로직은 Service로 모으는 것이 구조상 더 자연스럽습니다.

자주 막히는 포인트 | 초보자가 계층 구조에서 헷갈리는 부분
역할을 조금만 헷갈려도 코드가 금방 뒤섞이기 시작합니다.
1) Controller에 비즈니스 로직을 너무 많이 넣는 경우
원인 처음에는 작은 기능이라 보여서 검증, 계산, 저장까지 한 번에 넣기 쉽습니다.
해결 요청/응답 처리만 Controller에 두고 핵심 규칙은 Service로 옮기는 습관이 중요합니다.
2) Service 없이 Controller와 Repository를 바로 연결하는 경우
원인 단순 조회 정도는 바로 연결해도 된다고 생각하기 쉽습니다.
해결 처음부터 Service 계층을 두면 로직이 늘어날 때 구조를 유지하기 훨씬 쉽습니다.
3) Repository에 비즈니스 판단까지 넣는 경우
원인 조회 조건과 로직을 한 번에 처리하려다 보면 역할이 섞이기 쉽습니다.
해결 Repository는 데이터 접근에 집중하고, 정책 판단과 흐름 제어는 Service에 두는 편이 좋습니다.
설정/확인 체크리스트
  • Controller가 요청/응답 처리 외의 무거운 로직을 가지고 있지 않은가?
  • Service가 실제 업무 규칙과 흐름 제어를 담당하고 있는가?
  • Repository가 DB 접근 외의 책임까지 떠안고 있지 않은가?
  • DTO와 Entity 역할이 뒤섞이지 않았는가?
  • 재사용 가능한 로직이 Controller에 흩어져 있지 않은가?
소스코드 예제 | 회원 저장 기능도 계층별로 나누면 이렇게 보입니다
조회뿐 아니라 저장 기능도 같은 흐름으로 이해하면 구조가 더 선명해집니다.
@RestController
@RequestMapping("/members")
public class MemberController {

    private final MemberService memberService;

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

    @PostMapping
    public ResponseEntity<Void> saveMember(@RequestBody MemberRequest request) {
        memberService.saveMember(request);
        return ResponseEntity.ok().build();
    }
}
@Service
public class MemberService {

    private final MemberRepository memberRepository;

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

    public void saveMember(MemberRequest request) {
        Member member = new Member(request.getName(), request.getAge());
        memberRepository.save(member);
    }
}
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
}

이렇게 나누면 Controller는 API 입구 역할을 하고, Service는 핵심 로직을 담당하고, Repository는 저장에 집중하게 됩니다. 즉 각 계층이 무엇을 해야 하는지가 코드에서 훨씬 명확하게 드러납니다.

실무 포인트 | 계층 분리는 결국 변경에 강한 구조를 만드는 일이다
구조를 잘 나누면 기능 추가와 수정이 훨씬 덜 고통스럽습니다.
  • Controller는 얇게 유지할수록 테스트와 유지보수가 쉬워집니다.
  • Service는 재사용 가능한 업무 로직 중심으로 두는 편이 좋습니다.
  • Repository는 데이터 접근에만 집중시키는 것이 구조를 덜 흔들리게 만듭니다.
  • 계층 경계가 무너지면 작은 기능 수정도 여러 파일에서 꼬이기 쉽습니다.
초보자가 꼭 체크할 포인트
  • Controller는 요청을 받고 Service에 위임하는 역할에 가깝습니다.
  • Service는 비즈니스 로직의 중심입니다.
  • Repository는 DB 접근 계층입니다.
  • 세 계층을 나누는 이유는 역할 분리와 유지보수성 확보입니다.
FAQ
  • Q. Controller에서 Repository를 바로 호출해도 되나요?
    → 작은 예제에서는 동작할 수 있지만, 구조가 커질수록 로직이 섞여 유지보수가 어려워집니다. 보통은 Service를 두는 편이 더 안정적입니다.
  • Q. Service는 꼭 필요한가요?
    → 기능이 조금만 복잡해져도 거의 필요해집니다. 특히 검증, 조합 로직, 트랜잭션, 재사용 로직은 Service 계층이 있어야 관리가 쉽습니다.
  • Q. Repository에 비즈니스 로직을 넣으면 안 되나요?
    → Repository는 데이터 접근에 집중하는 편이 좋습니다. 정책 판단과 업무 흐름은 Service가 맡는 것이 일반적입니다.
  • Q. Spring MVC 구조와 계층형 구조는 같은 말인가요?
    → 완전히 같은 말은 아니지만, 실무에서는 Controller-Service-Repository 흐름을 Spring MVC 기반 계층 구조 설명과 함께 묶어서 이해하는 경우가 많습니다.
결론 | Spring MVC 구조는 역할 분리로 이해하는 것이 가장 쉽다
누가 무엇을 맡는지 분명해지면 코드도 훨씬 읽기 쉬워집니다.

Spring MVC 구조를 공부할 때 가장 중요한 것은 복잡한 이론보다 Controller, Service, Repository가 각각 무엇을 맡는지를 분명하게 구분하는 것입니다.

  • Controller는 요청을 받고 응답을 반환함
  • Service는 업무 로직을 처리함
  • Repository는 DB와 통신함
  • 요청은 보통 Controller → Service → Repository 순서로 흐름
  • 이 구조를 나누는 이유는 책임 분리와 유지보수성 확보
Spring MVC 구조는 결국 “한 클래스가 다 하지 않도록 역할을 나누는 구조”라고 이해하면 가장 오래 남습니다.
728x90
댓글