오늘은 Spring에서 자주 사용하는 3 Layer Architecture에 대해 정리했다.
처음에는 Controller에서 요청을 받고 바로 DB를 조회하거나, 로직까지 한 번에 처리해도 되는 것처럼 느껴졌는데, 실제로 코드를 작성해보니 역할을 나눠서 설계하는 이유가 분명했다.
3 Layer Architecture는 애플리케이션을 크게 다음 3개의 계층으로 나누는 구조이다.
Controller
Service
Repository
이 구조의 핵심은 각 클래스가 맡아야 하는 책임을 분리하는 것이다.
즉, 요청을 받는 역할, 비즈니스 로직을 처리하는 역할, DB와 통신하는 역할을 나누어서 코드를 더 읽기 쉽고 유지보수하기 쉽게 만드는 방식이다.
Controller는 클라이언트의 요청을 가장 먼저 받는 계층이다.
사용자가 브라우저나 Postman으로 요청을 보내면, 그 요청 URL과 HTTP Method에 맞는 Controller 메서드가 실행된다.
Controller가 하는 일은 보통 다음과 같다.
예를 들어 일정 전체 조회 API라면 Controller는 다음처럼 작성할 수 있다.
@GetMapping("/schedules")
public List<GetOneScheduleResponse> getAll(
@RequestParam(required = false) String name
) {
return scheduleService.getAll(name);
}
여기서 중요한 점은 Controller가 직접 비즈니스 로직을 처리하지 않는다는 것이다.
예를 들어 이름이 있으면 이름으로 조회하고, 없으면 전체 조회한다 같은 조건문을 Controller가 길게 들고 있으면 역할이 섞이게 된다.
Controller는 최대한 가볍게 두고,
요청 받기 -> Service에 넘기기 -> 결과 반환하기 흐름에 집중하는 것이 좋다.
내가 이해한 Controller의 역할은
입구 역할 또는 요청과 응답을 연결하는 역할에 가깝다.
Service는 3계층 구조에서 가장 핵심이 되는 계층이다.
실제 비즈니스 로직이 들어가는 곳이고, 애플리케이션이 “어떻게 동작할지”를 결정하는 곳이다.
예를 들면 다음과 같은 로직이 Service에 들어간다.
작성자명이 있으면 작성자명으로 조회, 없으면 전체 조회
비밀번호가 일치하는지 검증
일정 수정/삭제 가능 여부 판단
Entity를 DTO로 변환
트랜잭션 처리
직접짰던 예시 코드는 이런 느낌이다.
@Transactional(readOnly = true)
public List<GetOneScheduleResponse> getAll(String name) {
List<Schedule> schedules;
if (name == null || name.isBlank()) {
schedules = scheduleRepository.findAllByOrderByModifiedAtDesc();
} else {
schedules = scheduleRepository.findByNameOrderByModifiedAtDesc(name);
}
List<GetOneScheduleResponse> dtos = new ArrayList<>();
for (Schedule schedule : schedules) {
GetOneScheduleResponse dto = new GetOneScheduleResponse(
schedule.getId(),
schedule.getTitle(),
schedule.getContent(),
schedule.getName(),
schedule.getCreatedAt(),
schedule.getModifiedAt()
);
dtos.add(dto);
}
return dtos;
}
이 코드에서 보이듯이 Service는 단순히 Repository를 호출하는 것에서 끝나는 게 아니라,
어떤 조건으로 조회할지 결정하고
조회된 데이터를 어떤 형태로 응답할지 가공하고
필요하면 예외도 던지는
역할까지 담당한다.
즉 Service는
무엇을 할까?보다 어떻게 처리할까?를 담당하는 계층이라고 이해하면 좋다.
또한 @Transactional도 보통 Service에 붙인다.
이유는 데이터 변경 작업은 비즈니스 로직의 일부이기 때문이다.
예를 들어 수정 API에서 비밀번호를 확인하고, 일정 내용을 수정하는 작업 전체가 하나의 작업 단위이기 때문에 Service에서 트랜잭션을 관리하는 것이 자연스럽다.
Repository는 DB 접근을 담당하는 계층이다.
쉽게 말해 데이터를 저장하고 조회하는 역할만 하는 곳이다.
Spring Data JPA를 사용하면 JpaRepository를 상속하는 것만으로도 기본적인 CRUD 메서드를 자동으로 사용할 수 있다.
public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
}
이렇게만 해도 다음 메서드들을 사용할 수 있다.
findAll()
findById()
save()
deleteById()
existsById()
그리고 조건 조회나 정렬이 필요하면 메서드 이름 규칙에 맞춰 직접 커스텀할 수 있다.
public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
List<Schedule> findByName(String name);
}
여기서 느낀 점은 Repository는 정말 DB 접근만 담당해야 한다는 것이다.
조건 분기나 예외 처리, DTO 변환 같은 로직이 Repository에 들어가면 계층의 역할이 흐려진다.
즉 Repository는
“DB에 어떤 데이터를 요청할까?” 까지만 책임지는 것이 자연스럽다.
3계층 구조의 전체 흐름은 보통 다음과 같다.
Client -> Controller -> Service -> Repository -> DB
응답은 반대로 올라온다.
DB -> Repository -> Service -> Controller -> Client
예를 들어 일정 단건 조회를 생각해보면:
클라이언트가 /schedules/1 요청
Controller가 id=1을 받음
Service가 scheduleRepository.findById(1) 호출
Repository가 DB에서 조회
조회한 Entity를 Service가 DTO로 변환
Controller가 그 DTO를 응답으로 반환
이 흐름을 이해하고 나니 코드가 왜 분리되어야 하는지 조금 더 선명하게 보였다.
왜 굳이 클래스를 나눌 필요가 있나 싶었지만, 실제로는 나누는 이유가 분명했다.
첫 번째, 책임 분리가 된다
각 계층이 맡는 역할이 다르기 때문에 코드의 목적이 더 명확해진다.
Controller: 요청/응답 처리
Service: 비즈니스 로직 처리
Repository: DB 접근 처리
한 클래스가 모든 걸 다 하면 코드가 길어지고, 나중에 수정하기도 어려워진다.
두 번째, 유지보수가 쉬워진다
예를 들어 조회 조건이 바뀌면 Service를 수정하면 되고,
DB 조회 방식이 바뀌면 Repository를 수정하면 된다.
각 계층이 독립적으로 바뀔 수 있어서 수정 범위가 줄어든다.
세 번째, 재사용성이 높아진다
Service의 로직은 여러 Controller에서 재사용될 수 있다.
나중에 API가 늘어나도 같은 로직을 다시 활용할 수 있어서 중복을 줄일 수 있다.
네 번째, 테스트하기 좋다
역할이 분리되어 있으면 Service만 따로 테스트하거나, Repository만 따로 확인하기가 쉬워진다.
나중에 테스트 코드를 배울 때도 이 구조가 큰 장점이 될 것 같다.
내가 이해한 기준은 이렇다
요청 값 받기 → Controller
조건 판단, 예외 처리, DTO 변환 → Service
실제 조회/저장 → Repository
이 기준이 잡히니까 코드가 조금 덜 헷갈렸다 그리고 직접설정해보고 나누는게 생각보다 재밌었다
오늘 3 Layer Architecture를 정리하면서 느낀 점은
단순히 클래스를 여러 개로 나누는 것이 목적이 아니라,
각 코드가 맡을 책임을 명확하게 분리하는 것이 핵심이라는 것이다.
자바의 객체지향이랑 비슷한 느낌이였다
특히 Spring에서는 Controller, Service, Repository를 나누는 습관이 중요하다고 느꼈다.
처음에는 구조가 복잡해 보였지만, 오히려 분리하고 나니
코드가 읽기 쉬워지고
어디에서 무엇을 수정해야 하는지 명확해지고
에러가 났을 때도 어느 계층을 먼저 봐야 할지 감이 잡혔다.
아직은 어떤 로직을 Service에 두고, 어떤 부분을 Repository에 맡겨야 하는지 완전히 자연스럽지는 않지만,
이번 일정 관리 프로젝트를 통해 3계층 구조가 왜 필요한지 조금씩 이해하게 되었다.
3 Layer Architecture는 요청 처리, 비즈니스 로직, 데이터 접근을 분리하여 각 계층의 책임을 명확하게 만드는 구조이다.