TIL - 20260201

juni·2026년 1월 31일

TIL

목록 보기
255/316

0201 스프링 부트 프로젝트 (3/N): 계층형 아키텍처와 서비스 로직


✅ 1. 계층형 아키텍처 (Layered Architecture)

  • Spring 애플리케이션은 일반적으로 계층형 아키텍처를 따릅니다. 이는 각 부분이 특정 책임만 갖도록 코드를 구성하여, 시스템의 유지보수성과 확장성을 높이는 설계 방식입니다.

  • 주요 계층:

    1. Controller (Presentation Layer):

      • 책임: HTTP 요청을 받고, 응답을 보냄.
      • 역할: 사용자의 입력을 검증하고, 비즈니스 로직 처리를 서비스 계층에 위임.
    2. Service (Business Layer):

      • 책임: 애플리케이션의 핵심 비즈니스 로직 수행.
      • 역할: 여러 Repository를 조합하여 하나의 비즈니스 기능을 완성하거나, 트랜잭션을 관리.
    3. Repository (Persistence/Data Access Layer):

      • 책임: 데이터베이스에 직접 접근하여 데이터를 CRUD.
      • 역할: JpaRepository를 통해 데이터의 영속성을 처리.

✅ 2. DTO (Data Transfer Object)의 역할과 필요성

  • DTO란 계층 간에 데이터를 전달(Transfer)하는 데 사용되는 객체입니다.

  • 왜 엔티티(Entity)를 직접 사용하면 안 되는가?

    1. API 스펙의 불안정성: 엔티티는 DB 스키마와 1:1로 매핑됩니다. 엔티티 필드 변경 시, API 응답이 그대로 변경되어 클라이언트 앱에 오류를 유발할 수 있습니다.
    2. 불필요한 정보 노출: 엔티티에는 비밀번호나 내부 관리용 필드 등 외부에 노출되면 안 되는 민감한 정보가 포함될 수 있습니다.
    3. 양방향 연관관계 문제: 양방향으로 매핑된 엔티티를 JSON으로 변환할 때, 무한 루프에 빠져 스택 오버플로우가 발생할 수 있습니다.
    4. 요청/응답과 엔티티의 불일치: 회원가입 요청 시 필요한 데이터와 실제 User 엔티티의 필드가 정확히 일치하지 않는 경우가 많습니다.

➕ DTO의 활용

  • Request DTO: 컨트롤러가 클라이언트로부터 요청을 받을 때 사용하는 DTO. @Valid를 통한 유효성 검증 로직을 포함.
  • Response DTO: 컨트롤러가 클라이언트에게 응답을 보낼 때 사용하는 DTO. 엔티티에서 필요한 정보만 선별하여 담음.
// 게시글 생성을 위한 Request DTO
@Getter
@NoArgsConstructor
public class PostCreateRequest {
    private String title;
    private String content;

    // DTO를 Entity로 변환하는 메서드
    public Post toEntity() {
        return new Post(this.title, this.content);
    }
}

// 게시글 조회를 위한 Response DTO
@Getter
public class PostResponse {
    private Long id;
    private String title;
    private String content;
    private LocalDateTime createdAt;

    // Entity를 DTO로 변환하는 생성자
    public PostResponse(Post post) {
        this.id = post.getId();
        this.title = post.getTitle();
        this.content = post.getContent();
        this.createdAt = post.getCreatedAt();
    }
}

✅ 3. 서비스(Service) 계층 구현

  • 서비스 계층은 컨트롤러와 리포지토리 사이의 중재자 역할을 하며, 실제 비즈니스 로직을 처리합니다.

  • 주요 어노테이션:

    • @Service: 이 클래스가 비즈니스 로직을 담당하는 서비스 계층의 컴포넌트임을 Spring에 알립니다.
    • @Transactional: 메서드나 클래스에 트랜잭션을 적용합니다. 메서드 실행 중 예외 발생 시, 모든 DB 작업을 롤백하여 데이터 일관성을 보장합니다.
      • @Transactional(readOnly = true): 조회(SELECT)만 하는 메서드에 적용하여 성능을 최적화.
    • @RequiredArgsConstructor: final로 선언된 필드를 위한 생성자를 자동으로 만들어주어, 생성자 주입을 깔끔하게 구현할 수 있게 돕는 Lombok 어노테이션.
package com.example.myproject.service;

import com.example.myproject.domain.Post;
import com.example.myproject.dto.PostCreateRequest;
import com.example.myproject.dto.PostResponse;
import com.example.myproject.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor // final 필드에 대한 생성자 자동 생성
@Transactional(readOnly = true) // 클래스 전체에 읽기 전용 트랜잭션 기본 적용
public class PostService {

    private final PostRepository postRepository; // 생성자 주입

    // 게시글 생성 (쓰기 작업이므로 @Transactional로 덮어쓰기)
    @Transactional
    public Long createPost(PostCreateRequest request) {
        Post newPost = request.toEntity();
        Post savedPost = postRepository.save(newPost);
        return savedPost.getId();
    }

    // 단일 게시글 조회
    public PostResponse findPostById(Long id) {
        Post post = postRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("게시글을 찾을 수 없습니다. id=" + id));
        return new PostResponse(post);
    }

    // 모든 게시글 조회
    public List<PostResponse> findAllPosts() {
        return postRepository.findAll().stream()
                .map(PostResponse::new) // .map(post -> new PostResponse(post)) 와 동일
                .collect(Collectors.toList());
    }
}

✅ 4. 컨트롤러(Controller) 계층 구현

  • 컨트롤러는 서비스 계층을 호출하고, 그 결과를 받아 클라이언트에게 HTTP 응답을 보냅니다.
package com.example.myproject.controller;

import com.example.myproject.dto.PostCreateRequest;
import com.example.myproject.dto.PostResponse;
import com.example.myproject.service.PostService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;
import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/posts")
public class PostController {

    private final PostService postService;

    @PostMapping
    public ResponseEntity<Void> createPost(@RequestBody PostCreateRequest request) {
        Long postId = postService.createPost(request);
        // 생성된 리소스의 위치(URI)를 헤더에 담아 201 Created 응답
        return ResponseEntity.created(URI.create("/api/posts/" + postId)).build();
    }

    @GetMapping("/{id}")
    public ResponseEntity<PostResponse> getPost(@PathVariable Long id) {
        PostResponse post = postService.findPostById(id);
        return ResponseEntity.ok(post);
    }

    @GetMapping
    public ResponseEntity<List<PostResponse>> getAllPosts() {
        List<PostResponse> posts = postService.findAllPosts();
        return ResponseEntity.ok(posts);
    }
}

📌 요약

  • Spring 애플리케이션은 Controller - Service - Repository계층형 아키텍처를 따르며, 각 계층은 명확한 책임을 가집니다.
  • 엔티티를 API 계층에 직접 노출하는 것은 매우 위험하며, 반드시 Request/Response DTO를 사용하여 계층 간 데이터를 안전하고 유연하게 전달해야 합니다.
  • 서비스 계층@Service 어노테이션을 통해 비즈니스 로직을 구현하고, @Transactional 어노테이션으로 데이터의 일관성을 보장하는 핵심적인 역할을 합니다.
  • 컨트롤러는 서비스 계층의 결과를 받아, ResponseEntity를 통해 적절한 HTTP 상태 코드와 함께 클라이언트에게 응답합니다.

0개의 댓글