Spring 애플리케이션은 일반적으로 계층형 아키텍처를 따릅니다. 이는 각 부분이 특정 책임만 갖도록 코드를 구성하여, 시스템의 유지보수성과 확장성을 높이는 설계 방식입니다.
주요 계층:
Controller (Presentation Layer):
Service (Business Layer):
Repository (Persistence/Data Access Layer):
JpaRepository를 통해 데이터의 영속성을 처리.DTO란 계층 간에 데이터를 전달(Transfer)하는 데 사용되는 객체입니다.
왜 엔티티(Entity)를 직접 사용하면 안 되는가?
User 엔티티의 필드가 정확히 일치하지 않는 경우가 많습니다.@Valid를 통한 유효성 검증 로직을 포함.// 게시글 생성을 위한 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();
}
}
서비스 계층은 컨트롤러와 리포지토리 사이의 중재자 역할을 하며, 실제 비즈니스 로직을 처리합니다.
주요 어노테이션:
@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());
}
}
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);
}
}
@Service 어노테이션을 통해 비즈니스 로직을 구현하고, @Transactional 어노테이션으로 데이터의 일관성을 보장하는 핵심적인 역할을 합니다.ResponseEntity를 통해 적절한 HTTP 상태 코드와 함께 클라이언트에게 응답합니다.