Spring : MVC 패턴으로 API 만들기

최혜린·2025년 3월 16일

이전 글에서 Model을 Entity, DTO, Service, Controller로 세분화하는 이유에 대해 다뤘다.
이번에는 이 구조를 실제 프로젝트에 어떻게 적용하는지 정리하려한다.


백엔드 개발 순서 & 구조

Entity → DTO → Repository → Service → Controller 순서로 개발 진행.

백엔드 개발 순서를 이렇게 짠 이유는?

  • Entity: 먼저 도메인/테이블 구조가 확정되어야 DB와 연동 가능
  • DTO: Entity를 외부와 직접 연결하지 않고, DTO를 통해 API 데이터 구조를 정의
  • Repository: Entity가 준비된 후, JPA로 DB 작업을 수행할 수 있도록 Repository 설계
  • Service: DB 작업 + 비즈니스 로직을 묶어 비즈니스 계층 완성
  • Controller: HTTP 요청을 받고, Service 호출로 깔끔하게 끝내기 위해 마지막에 작성

1. Entity - DB 테이블 구조 정의

  • Entity는 DB의 테이브과 매핑되는 도메인 모델이다.
@Entity
@Table(name = "community")
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class Community {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false, columnDefinition = "TEXT")
    private String content;

    private String author;
    private boolean isHidden;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    @PrePersist
    public void prePersist() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    public void preUpdate() {
        this.updatedAt = LocalDateTime.now();
    }
}
  • Entity에서는 실제 DB 테이블과 매핑되는 필드를 정의하고, @PrePersist, @PreUpdate로 날짜 자동 처리도 포함.

2. DTO - 요청/응답 데이터 분리

  • Entity와 별도로 API 요청/응답에서 사용할 데이터 전송 객체를 정의한다.
@Data
public class CommunityRequestDTO {
    private String title;
    private String content;
    private String author;
    private boolean isHidden;
}

@Data @Builder
public class CommunityResponseDTO {
    private Long id;
    private String title;
    private String content;
    private String author;
    private boolean isHidden;
    private LocalDateTime createdAt;
}
  • Entity를 직접 외부에 노출하지 않고, DTO를 통해 필요한 데이터만 주고받도록 분리.

3. Repository - DB와 직접 통신

  • Repository는 JPA를 토해 DB에 CRUD 쿼리를 수행한다.
@Repository
public interface CommunityRepository extends JpaRepository<Community, Long> {
    List<Community> findByIsHiddenFalse();
}
  • JPA가 제공하는 기본 메소드(save, findAll, deleteById)를 활용하고, 숨김 처리된 게시글 제외 조회는 커스텀 메소드로 정의.

4. Service - 비지니스 로직 처리

  • 비지니스 로직을 Service 계층에서 처리한다.
  • Service는 비즈니스 로직DB 작업(Repository 호출)을 묶는 계층
@Service
@RequiredArgsConstructor
public class CommunityService {
    private final CommunityRepository communityRepository;

    public Long createPost(CommunityRequestDTO dto) {
        Community post = Community.builder()
                .title(dto.getTitle())
                .content(dto.getContent())
                .author(dto.getAuthor())
                .isHidden(dto.isHidden())
                .build();
        return communityRepository.save(post).getId();
    }

    public List<CommunityResponseDTO> getAllPosts() {
        return communityRepository.findByIsHiddenFalse()
                .stream()
                .map(this::toDTO)
                .toList();
    }

    public void updatePost(Long id, CommunityRequestDTO dto) {
        Community post = communityRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Post not found"));
        post.setTitle(dto.getTitle());
        post.setContent(dto.getContent());
        post.setIsHidden(dto.isHidden());
        communityRepository.save(post);
    }

    public void deletePost(Long id) {
        communityRepository.deleteById(id);
    }

    private CommunityResponseDTO toDTO(Community post) {
        return CommunityResponseDTO.builder()
                .id(post.getId())
                .title(post.getTitle())
                .content(post.getContent())
                .author(post.getAuthor())
                .isHidden(post.isHidden())
                .createdAt(post.getCreatedAt())
                .build();
    }
}
  • Service에서 Entity와 DTO의 변환을 담당하면서 비즈니스 로직도 처리.

5. Controller - 사용자 요청 처리

  • 최종적으로 HTTP 요청을 받고 응답을 반환하는 계층
@RestController
@RequestMapping("/api/community")
@RequiredArgsConstructor
public class CommunityController {

    private final CommunityService communityService;

    @PostMapping
    public ResponseEntity<Long> create(@RequestBody CommunityRequestDTO dto) {
        Long id = communityService.createPost(dto);
        return ResponseEntity.ok(id);
    }

    @GetMapping
    public ResponseEntity<List<CommunityResponseDTO>> getAll() {
        return ResponseEntity.ok(communityService.getAllPosts());
    }

    @PutMapping("/{id}")
    public ResponseEntity<Void> update(@PathVariable Long id, @RequestBody CommunityRequestDTO dto) {
        communityService.updatePost(id, dto);
        return ResponseEntity.ok().build();
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        communityService.deletePost(id);
        return ResponseEntity.ok().build();
    }
}
  • Controller는 요청을 받아 Service를 호출하고, HTTP 상태코드와 함께 응답.

Spring MVC 계층별 책임 정리

  • Controller: 사용자 요청과 응답만 담당 (비즈니스 로직 없음 ❌), REST API 라우터처럼 요청과 응답을 중개하는 것에 집중
  • Service: 비즈니스 로직을 담당하고, 필요할 때 - Repository를 통해 DB와 통신 (JPA 호출은 여기서)
  • Repository: 오직 DB 작업만 수행하는 계층 (JPA 메소드 정의) , 비지니스 로직은 없음 ❌
  • DB: 물리적으로 데이터를 저장하는 곳
Client (브라우저, 앱)
        │
        ▼
[Controller]
- 요청 수신 (POST /api/community)
        │
        ▼
[Service]
- 비즈니스 로직 처리
- Repository 호출
        │
        ▼
[Repository]
- JPA로 DB 접근 (save, findById 등)
        │
        ▼
[Database]
- 실제 데이터 저장 / 조회 / 삭제
        │
        ▲
        └── 최종 응답 반환


이번 글에서는 API를 개발하면서 MVC 패턴을 어떻게 적용하는지 알아보았다.
Entity부터 Controller까지 계층을 명확히 구분하고, 각 계층의 역할에 맞게 책임을 나누어 구현하면 유지보수성확장성을 모두 확보할 수 있다.

+ 프로젝트에서 만든 코드지만.. 아직도 API를 만드는건 어렵다.. 막상 하려면 Service에서 막히는 😂
열띠미 하자..!!

profile
산으로 가는 코딩.. 등산 중..🌄

0개의 댓글