티스토리 뷰

패턴의 개요

  • 현대 스프링 애플리케이션은 관심사의 분리(Separation of Concerns)를 통해 유지보수성과 확장성을 높임
  • 그 핵심 설계 방식 중 하나가 Controller, Service, Repository 패턴
  • 애플리케이션의 각 계층이 명확한 역할을 가지고 상호작용하여, 기능의 응집도를 높이고 결합도를 낮추는 데 도움을 줌


각 계층의 역할 및 책임

Controller

역할

  • 클라이언트의 요청을 받아 처리하고, 응답을 반환하는 역할

주요 책임

  • HTTP 요청 매핑 (GET, POST, PUT, DELETE 등)
  • 요청 파라미터 유효성 검사 및 바인딩
  • Service 계층 호출을 통한 비즈니스 로직 처리 위임
  • 결과 데이터를 View 또는 JSON/XML 형식으로 반환

특징

  • 프레젠테이션 계층(Presentation Layer)에 해당
  • 비즈니스 로직은 포함하지 않고 요청과 응답에 집중

Service

역할

  • 비즈니스 로직을 처리하는 핵심 계층

주요 책임

  • 여러 Repository 및 외부 API 호출을 통한 업무 처리
  • 트랜잭션 관리 (스프링의 @Transactional 활용)
  • 도메인 로직의 캡슐화 및 재사용성 보장

특징

  • 비즈니스 계층(Business Layer)으로, Controller와 데이터 계층 사이의 중간자 역할
  • 복잡한 연산, 규칙 검증, 데이터 조작 등을 담당

Repository

역할

  • 데이터 접근(Data Access) 계층

주요 책임

  • 데이터베이스와의 CRUD(Create, Read, Update, Delete) 연산 수행
  • ORM(Object Relational Mapping) 프레임워크 (예: JPA, MyBatis)와의 연동
  • 데이터 소스의 추상화 및 접근 로직 캡슐화

특징

  • 데이터베이스에 직접 접근하여 쿼리를 실행하는 역할
  • 도메인 객체와의 매핑을 통해 객체 지향적인 데이터 처리를 지원

 

코드 흐름

1. 요청 발생 (Controller)

  • 클라이언트가 HTTP 요청(예: /users)을 보내면, Controller가 이를 받아 처리

2. 비즈니스 로직 처리 (Service)

  • Controller는 요청을 Service 계층에 전달하고, Service는 유효성 검사, 비즈니스 규칙 검증, 여러 Repository 호출 등을 수행

3. 데이터 처리 (Repository)

  • Service가 호출한 Repository는 데이터베이스와 상호작용하여 CRUD 작업을 수행

4. 응답 반환

  • 처리된 결과를 Service가 Controller에 전달하고, Controller는 최종적으로 클라이언트에게 응답

장점

✅ 관심사의 분리

  • 각 계층이 독립적인 책임을 가지므로 코드의 가독성과 유지보수성이 향상됨

✅ 재사용성 증가

  • Service 계층의 로직은 여러 Controller에서 재사용 가능

✅ 테스트 용이성

  • 각 계층을 독립적으로 단위 테스트할 수 있어 안정적인 개발 및 배포가 가능함

✅ 트랜잭션 관리

  • Service 계층에서 트랜잭션 처리를 집중 관리할 수 있어 데이터 무결성 보장

단점 및 주의사항

❌ 초기 설계 복잡성

  • 단순한 애플리케이션에서는 계층을 분리함으로써 오히려 복잡성이 증가할 수 있음

❌ 오버엔지니어링 위험

  • 너무 과도한 분리로 인해 작은 기능에도 여러 계층이 필요할 수 있음

❌ 계층 간 의존성 관리

  • 각 계층 간의 의존성이 명확하게 정의되어야 하며, 순환 참조를 피해야 함

실무 예시 상황: 사용자 관리 시스템

예시 시나리오

  • 가상의 사용자 관리 시스템에서는 사용자의 회원 가입, 정보 조회, 수정, 삭제 기능을 제공
  • 이 시스템을 통해 실무에서 DTO 사용, AOP 기반 로깅, 예외 처리, 의존성 주입 등의 실무 팁을 어떻게 적용할 수 확인

요구 사항

  • 회원 가입: 사용자가 회원 가입 시 입력한 정보(예: 이름, 이메일, 비밀번호)를 받아 저장
  • 회원 정보 조회: 등록된 사용자의 정보를 조회할 수 있음
  • 데이터 검증 및 예외 처리: 입력 데이터의 유효성을 검증하고, 문제가 발생하면 사용자에게 명확한 에러 메시지를 반환
  • 로깅 및 모니터링: 각 계층에서 발생하는 주요 이벤트를 로깅하여 추후 디버깅에 활용

실무 팁 적용

  1. DTO(Data Transfer Object) 사용
    • Controller에서는 클라이언트와의 데이터 교환을 위해 UserRequestDtoUserResponseDto를 사용
    • 이를 통해 도메인 엔티티의 직접 노출을 막고, 데이터 검증 및 보안을 강화
  2. 의존성 주입(Dependency Injection)
    • 스프링의 IoC 컨테이너를 이용해 Controller, Service, Repository 간의 의존성을 주입하여 느슨한 결합(loose coupling)을 유지
  3. AOP(Aspect-Oriented Programming) 활용
    • 서비스 호출 전후에 로깅이나 성능 모니터링, 예외 처리를 AOP를 통해 분리하여 코드의 가독성을 높이고, 횡단 관심사를 효율적으로 관리
    • 예를 들어, @Aspect 어노테이션을 사용하여 모든 Service 메서드 호출 시 로그를 남길 수 있음
  4. 예외 처리 전략
    • 각 계층에서 발생하는 예외를 글로벌 예외 처리(@ControllerAdvice)를 통해 통합 관리하고, 클라이언트에게 일관된 에러 응답을 제공합니다.
    • Service 계층에서는 필요에 따라 트랜잭션 롤백을 자동으로 수행하도록 @Transactional을 사용합니다.

구현 예시

// DTO 클래스
public class UserRequestDto {
    @NotBlank
    private String name;
    @Email
    private String email;
    @NotBlank
    private String password;
    // getter, setter
}

public class UserResponseDto {
    private Long id;
    private String name;
    private String email;
    // 생성자: User 엔티티를 받아 필요한 필드 매핑
}
// Controller 예시
@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService userService;
    
    // 생성자 주입
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @PostMapping
    public ResponseEntity<UserResponseDto> createUser(@Valid @RequestBody UserRequestDto dto) {
        User user = userService.createUser(dto);
        return ResponseEntity.status(HttpStatus.CREATED)
                             .body(new UserResponseDto(user));
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<UserResponseDto> getUser(@PathVariable Long id) {
        User user = userService.getUserById(id);
        return ResponseEntity.ok(new UserResponseDto(user));
    }
}
// Service 예시
@Service
@Transactional
public class UserService {

    private final UserRepository userRepository;
    
    // 생성자 주입
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public User createUser(UserRequestDto dto) {
        // 입력 데이터 검증 및 비즈니스 로직 처리
        User user = new User(dto.getName(), dto.getEmail(), dto.getPassword());
        return userRepository.save(user);
    }
    
    public User getUserById(Long id) {
        return userRepository.findById(id)
                             .orElseThrow(() -> new ResourceNotFoundException("User not found"));
    }
}
// Repository 예시 (Spring Data JPA 활용)
public interface UserRepository extends JpaRepository<User, Long> {
    // 추가적인 쿼리 메서드 정의 가능
}
// AOP 기반 로깅 예시
@Aspect
@Component
public class LoggingAspect {
    
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        logger.info("Entering {} with arguments {}", joinPoint.getSignature(), joinPoint.getArgs());
    }
    
    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
    public void logAfter(JoinPoint joinPoint, Object result) {
        logger.info("Exiting {} with result {}", joinPoint.getSignature(), result);
    }
}

실무 상황 적용 요약

데이터 전달 시 DTO 사용

  • Controller와 Service 간에는 도메인 엔티티 대신 DTO를 사용해 보안 및 데이터 무결성을 유지

의존성 주입 및 계층 간 경계 엄격 유지

  • Controller에서는 Service만 호출하고, Service는 Repository만 호출하는 명확한 계층 구조를 유지

트랜잭션 및 예외 관리

  • Service 계층에서 @Transactional을 활용해 데이터 변경 시 원자성을 보장하며, 글로벌 예외 처리(@ControllerAdvice)를 통해 에러 상황에 대한 일관된 응답을 제공

AOP를 통한 횡단 관심사 분리

  • 로깅, 모니터링, 성능 측정 등의 기능을 AOP로 분리하여 각 계층의 코드가 핵심 로직에 집중할 수 있도록 함

면접 대비 질문

더보기

기본 질문

Q1. Controller, Service, Repository 패턴의 기본 역할과 장점을 설명하세요.

모범 답변
Controller는 클라이언트의 요청을 받아 응답하는 프레젠테이션 계층이며, Service는 비즈니스 로직과 트랜잭션 관리를 담당하는 계층, Repository는 데이터베이스와의 CRUD 연산을 처리하는 데이터 접근 계층입니다. 이 패턴은 각 계층 간의 책임 분리를 통해 유지보수성과 테스트 용이성을 크게 향상시킵니다.


Q2. 왜 스프링 애플리케이션에서 계층화를 사용하는가요?

모범 답변
계층화를 통해 코드의 응집도를 높이고 결합도를 낮출 수 있습니다. 이는 코드의 재사용성과 가독성을 향상시키며, 문제 발생 시 특정 계층만 집중적으로 디버깅할 수 있어 개발 및 유지보수에 유리합니다.


심화 질문

Q1. Service 계층에서 트랜잭션 관리를 어떻게 처리하며, 그 이유는 무엇인가요?

모범 답변
Service 계층은 @Transactional 어노테이션을 활용하여 트랜잭션을 관리합니다. 이는 비즈니스 로직을 수행하는 동안 여러 Repository 호출에 대한 원자성(atomicity)을 보장하여 데이터의 무결성을 유지하기 위함입니다.


Q2. 단위 테스트를 진행할 때 각 계층은 어떻게 테스트하는 것이 좋은가요?

모범 답변

  • Controller: MockMvc 등을 활용하여 HTTP 요청 및 응답을 시뮬레이션합니다.
  • Service: 비즈니스 로직에 집중하여, Repository를 목(mock) 객체로 주입해 단위 테스트를 수행합니다.
  • Repository: 실제 데이터베이스나 인메모리 데이터베이스(H2 등)를 사용하여 CRUD 연산을 검증합니다.

압박 질문

Q1. 계층 분리 없이 모든 로직을 Controller에 작성하는 경우 발생할 수 있는 문제점은 무엇인가요?

모범 답변
모든 로직을 Controller에 작성하면 코드의 응집도가 낮아지고, 유지보수성이 크게 저하됩니다. 또한, 테스트가 어려워지고, 비즈니스 로직과 프레젠테이션 로직이 혼재되어 재사용성이 떨어집니다.


Q2. 만약 Repository 계층에서 데이터베이스에 직접 의존성이 강하게 발생한다면 어떻게 개선할 수 있을까요?

모범 답변
Repository 인터페이스를 도입하여 구현체와의 의존성을 낮추고, Spring Data JPA와 같은 프레임워크를 활용하면 데이터 접근 로직의 추상화를 통해 테스트와 유지보수가 용이해집니다.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함