Spring : MVC 구조 - Entity, DTO, Service, Repository는 왜 필요할까?

최혜린·2025년 3월 16일

MVC(Model-View-Controller)는 소프트웨어 설계 패턴 중 하나로, 비지니스 로직과 UI를 분리하여 유지 보수성과 확장성을 높이는 구조이다.

1. MVC 구성 요소

  1. Model(모델)
    데이터 및 비지니스 로직 담당
    데이터베이스와의 연동 및 연산 처리
    ex) DAO(Data Access Object, Repository), Service 클래스

※ 참고: DAO와 Repository는 같은 의미로 사용된다.

  • DAO(Data Access Object): 전통적인 용어
  • Repository: Spring Data JPA에서 사용하는 용어
  1. View(뷰)
    사용자에게 화면을 제공
    모델의 데이터를 출력하고 입력을 받아 컨트롤러에 전달
    ex ) HTML, JSP, Thymeleaf

  2. Controller(컨트롤러)
    사용자의 요청을 처리하고 모델과 뷰를 연결
    요총을 모델에 전달하고, 모델에서 받은 데이터를 뷰에 전달
    ex) Spring의 @Controller, @RestController


Spring MVC에서 Model은 데이터와 비즈니스 로직을 담당하는 핵심 요소다.
하지만 실무에서는 단순히 Model로만 처리하지 않고, 이를 Entity, DTO, Service, Repository 등으로 세분화한다.

👉 왜 이런 세분화가 필요할까?
이 글에서는 각 계층이 필요한 이유와 그 역할에 대해 설명한다.


2. Model을 Entity, DTO, Service, Repository로 세분화 하는 이유

Spring MVC의 Model은 원래 데이터와 비즈니스 로직을 담당하는 역할이다.
하지만 실무에서는 Model을 Entity, DTO, Service, Repository 로 세분화해서 사용한다.
이렇게 나누는 이유는 책임 분리를 지키고 유지보수성을 높이기 위해서이다.


1. Entity와 DTO를 분리하는 이유

🔴 Entity를 그대로 사용하면 안되는 이유

  • Entity는 데이터베이스와 직접 연결되어있어 API 응답에 Entity를 직접 반환하면 보안 문제가 발생할 수 있다.
  • 예를 들어, User 엔티티가 비밀번호 필드를 포함하고 있다면, 이 데이터를 API 응답에 그대로 보내는 실수를 할 수도 있다.
  • 또한 Entity는 DB 스키마와 1:1 매칭되므로, 프론트엔드에서 원하는 데이터 구조와 맞지 않을 수 있다.
@Entity
@Table(name = "users")
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    private String password; // 보안상 노출되면 안 되는 필드
}

🟢 DTO를 사용하는 이유

  • 필요한 데이터만 포함하여 가공 가능하다.
  • Entity와 View를 분리하여 유지보수성을 높인다.
  • API 응답 형식을 유연하게 변경 가능하다.
public class UserDTO {
    private String name;
    private String email;

    public UserDTO(User user) {
        this.name = user.getName();
        this.email = user.getEmail();
    }
}

Entity에는 비밀번호(password)가 있지만, DTO에서는 제외하여 보안을 강화할 수 있다


2. Service와 Repository를 분리하는 이유

🔴 Controller에서 직접 Repository를 호출하면 안되는 이유

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
    private final UserRepository userRepository;

    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        return ResponseEntity.ok(userRepository.findById(id).orElse(null));
    }
}
  1. 컨트롤러가 비지니스 로직을 가지게 된다.
    컨트롤러는 원래 요청을 받아서 적절한 계층(Service)에 전달하고, 그 결과를 View(프론트엔드)로 반환하는 역할이다.
    하지만 위와 같이 Controller에서 직접 Repository에서 데이터를 가져오는 역할까지 하면 컨트롤러의 책임이 커지고 유지보수가 어려워진다.

컨트롤러는 단순히 요청을 받고, 데이터 처리와 비지니스 로직은 서비스에서 담당해야한다.


  1. 비지니스 로직을 추가하기 어렵다.
    컨트롤러에서 직접 Repository를 호출하면, 추가적인 로직이 필요한 경우 컨트롤러에서 처리해야한다.
    컨트롤러에 직접 로직을 추가할 경우 다른 API에서 동일한 로직이 필요한 경우 중복된 코드가 많아진다.
    때문에 유지보수가 어렵고 비지니스 로직이 흩어져서 코드의 일관성이 깨진다.
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
    private final UserRepository userRepository;

    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        User user = userRepository.findById(id).orElse(null);

        if (user == null || !user.isActive()) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
        }

        return ResponseEntity.ok(user);
    }
}

이런 검증 로직은 컨트롤러가 아닌 서비스 계층에서 처리해야 한다.

  1. 다른 기능 추가 시 재사용이 어렵다
    컨트롤러에서 직접 리포지토리를 호출하면 기능을 확장하기 어렵다. 예를 들어, 특정 사용자의 정보를 가져올 때, 추가적인 데이터(예: 사용자의 주문 정보)도 함께 가져와야 한다면 추가 데이터를 가져오려면 컨트롤러를 직접 수정해야 한다. 하지만 Service를 사용하면, 동일한 메서드를 재사용하면서 로직을 추가하기 쉽다.

3. Repository를 따로 두는 이유

  1. 데이터 접근을 추상화하여 유지보수성을 높이기 위해
  • Service가 Repository를 통해 데이터를 가져오면 DB가 바뀌어도 Service 로직은 그대로 유지된다.
  • Repository 계층을 따로 두면 JPA, MyBatis 등 다양한 방식으로 쉽게 변경 가능하다.
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
  • JAPRepository를 사용하면 기본적인 CRUD 메서드를 제공한다.
  • 만약 나중에 DB를 바꿔야 한다면, JPARepository 대신 MongoRepository로 변경 가능

데이터베이스와 직접 연결되는 Repository는 역할이 분리되어야 유지보수가 쉬움


4. Service를 활용하는 이유

  • Controller는 요청을 받고, 비즈니스 로직 처리는 Service에서 수행한다.
  • Service는 여러 Repository를 조합하여 로직을 수행할 수 있다.
  • 비즈니스 로직을 한 곳에서 관리하므로 유지보수가 쉬워진다.
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @GetMapping("/{id}")
    public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
        return ResponseEntity.ok(userService.getUserById(id));
    }
}
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;

   public UserDTO getUserById(Long id) {
    User user = userRepository.findById(id)
            .orElseThrow(() -> new RuntimeException("User not found"));

    // 예: 닉네임이 없는 경우 기본값 처리
    if (user.getName() == null || user.getName().isBlank()) {
        user.setName("Guest");
    }

    return new UserDTO(user);
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
  • Controller는 단순히 요청을 받고, 비즈니스 로직은 Service에서 처리된다.
  • Service에서 데이터를 가공하여 DTO로 변환 후 반환한다.

Model이 실무에서 Entity, DTO, Service, Repository로 세분화되는 이유는 다음과 같다.

분리된 요소이유
Entity vs DTOEntity를 그대로 사용하면 보안 문제 발생 → DTO를 통해 필요한 데이터만 전달
Controller vs ServiceController가 직접 Repository를 호출하면 역할이 혼재됨 →Service에서 비지니스 로직을 처리
Service vs RepositoryDB가 변경될 때 대응하기 위해 Repository 계층을 따로 둠
profile
산으로 가는 코딩.. 등산 중..🌄

0개의 댓글