상세 컨텐츠

본문 제목

자바 백엔드 기본 구조 이해하기: Controller - Service - Repository, 그리고 JPA vs MyBatis

공부/Java 공부

by hongga 2026. 9. 6. 10:30

본문

프론트엔드를 하다 보면 백엔드 코드가 "왜 이렇게 폴더가 나뉘어 있지?"라는 생각이 들 때가 있습니다. React에서 컴포넌트-훅-API 호출을 분리하듯, 스프링(Spring) 기반 자바 백엔드도 역할별로 계층을 나눕니다. 이번 글에서는 가장 기본이 되는 Controller - Service - Repository 구조와, 데이터베이스에 접근하는 두 가지 방식인 JPAMyBatis를 정리해봅니다.


1. 왜 계층을 나누는가

한 파일에 요청 처리, 비즈니스 로직, SQL을 다 몰아넣을 수도 있습니다. 하지만 그렇게 하면:

  • 코드를 수정할 때 어디까지 영향이 가는지 파악하기 어렵고
  • 테스트하기가 힘들고
  • 나중에 DB를 바꾸거나 API 스펙이 바뀔 때 손댈 범위가 커집니다

그래서 역할을 세 계층으로 나눠서, 각 계층이 자기 책임만 지도록 만듭니다.

위 그림처럼 요청은 Client → Controller → Service → Repository → DB 순서로 흘러가고, 응답은 반대 방향으로 돌아옵니다.


2. Controller — 요청의 문지기

Controller는 클라이언트(브라우저, 앱)의 HTTP 요청을 가장 먼저 받는 계층입니다.

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
        UserResponse user = userService.findUser(id);
        return ResponseEntity.ok(user);
    }
}

Controller가 하는 일

  • URL과 HTTP 메서드(GET, POST 등)를 매핑
  • 요청 파라미터/바디를 자바 객체로 변환하고 기본 검증
  • Service를 호출하고, 결과를 HTTP 응답 형태로 감싸서 반환

Controller가 하지 않는 일

  • SQL 작성, 트랜잭션 관리, 복잡한 비즈니스 규칙 판단 — 이건 모두 Service의 몫입니다

프론트엔드로 비유하면, Controller는 라우터(router)에 가깝습니다. "이 URL로 오면 이 로직을 실행해줘"라고 연결만 해주고, 실제 계산은 다른 곳에 위임하는 역할입니다.


3. Service — 비즈니스 로직의 중심

Service는 실제 업무 규칙이 담기는 곳입니다. "포인트를 적립한다", "주문을 취소하면 재고를 되돌린다" 같은 로직이 여기서 처리됩니다.

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    @Transactional
    public UserResponse findUser(Long id) {
        User user = userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));
        return UserResponse.from(user);
    }
}

Service가 하는 일

  • 여러 Repository를 조합해서 하나의 업무 단위를 완성
  • @Transactional로 "여기까지는 전부 성공하거나, 전부 실패해야 한다"는 트랜잭션 경계를 설정
  • 도메인 규칙(할인 정책, 권한 체크 등) 처리

Service는 HTTP가 뭔지 몰라도 되고, SQL 문법도 몰라도 됩니다. 오직 "업무가 어떻게 돌아가야 하는지"만 압니다.


4. Repository — 데이터베이스 접근 담당

Repository는 DB와 직접 대화하는 계층입니다. 여기서 등장하는 것이 JPAMyBatis입니다. 둘 다 "자바 객체 ↔ DB 테이블"을 연결해주는 기술이지만, 접근 방식이 다릅니다.

JPA (Java Persistence API)

  • 객체 중심입니다. 자바 객체(Entity)를 저장하거나 조회하면, 내부적으로 SQL을 자동 생성해줍니다.
  • Spring Data JPA를 쓰면 userRepository.findById(id)처럼 메서드 이름만으로 쿼리가 만들어집니다.
  • CRUD가 많은 일반적인 서비스에서 생산성이 매우 높습니다.
  • 다만 복잡한 통계 쿼리나 여러 테이블을 얽는 조회에서는 오히려 직접 SQL을 짜는 것보다 까다로울 수 있습니다.

MyBatis

  • SQL 중심입니다. 개발자가 XML이나 어노테이션에 SQL을 직접 작성하고, 결과를 자바 객체로 매핑합니다.
  • 실행되는 쿼리를 정확히 통제할 수 있어서, 튜닝이 필요하거나 레거시 DB 구조를 다뤄야 할 때 강점이 있습니다.
  • 다만 테이블 구조가 바뀌면 관련된 SQL을 일일이 찾아 수정해야 하는 부담이 있습니다.

그래서 뭘 써야 할까?

상황 선택하기 좋은 기술

신규 서비스, 일반적인 CRUD가 많은 경우 JPA
복잡한 조회/통계, 세밀한 쿼리 튜닝이 필요한 경우 MyBatis
금융권 등 레거시 DB·기존 SQL 자산이 많은 경우 MyBatis (또는 JPA와 혼용)

실무에서는 둘 중 하나만 쓰기보다, 기본 CRUD는 JPA로, 복잡한 리포트성 쿼리는 MyBatis로 함께 쓰는 경우도 흔합니다.


5. 전체 흐름 한 번에 정리

Client (React/Vue 등)
   │  HTTP 요청 (GET /api/users/1)
   ▼
Controller           ← "이 URL 요청이 왔으니 Service한테 넘기자"
   │
   ▼
Service               ← "권한 확인하고, 트랜잭션 열고, 로직 처리하자"
   │
   ▼
Repository (JPA/MyBatis) ← "DB에서 실제로 데이터를 가져오자"
   │
   ▼
DB (MariaDB, PostgreSQL...)

각 계층은 자신의 바로 아래 계층만 알면 됩니다. Controller는 Service만 알고, Service는 Repository만 압니다. 덕분에 예를 들어 DB를 MariaDB에서 PostgreSQL로 바꾸더라도, Repository 구현체만 손보면 되고 Controller나 Service 코드는 건드릴 필요가 없어집니다.


마무리

Controller-Service-Repository 구조는 결국 "누가 무엇을 몰라도 되게 만들 것인가"를 설계하는 것입니다. 프론트엔드에서 관심사를 분리하듯, 백엔드도 같은 원칙을 계층이라는 형태로 구현한 셈입니다. JPA와 MyBatis는 이 중 Repository 계층에서 선택하는 도구일 뿐, 정답이 정해져 있다기보다는 프로젝트 상황에 맞게 고르는 것이라고 이해하면 됩니다.

 

관련글 더보기