📌 기본 정보
- 주차/주제: 6주차 - 비즈니스 로직 구현과 서비스 레이어
📚 학습 내용 요약
1. Service 레이어의 역할과 책임 조사하기
- Layer는 관심사의 집합

1. Presentation Layer (프레젠테이션 계층)
- 사용자(UI) 요청 받기: 웹브라우저·앱·윈도우 애플리케이션 등 어디서든 들어오는 요청을 처리
- 응답(View) 반환: 처리 결과를 HTML 페이지나 JSON 등으로 포장해 사용자에게 돌려줌
- 입력 검증·예외 처리: URL 잘못 호출, 필수 파라미터 누락 같은 “화면 관점” 오류를 잡아서 사용자에게 친절한 메시지 제공
2. Service Layer (서비스 계층)
- 순수 비즈니스 로직 구현
- 트랜잭션 경계 관리 (@Transactional)
- 모두 성공시키거나, 모두 취소(rollback)

- 모두 성공시키거나, 모두 취소(rollback)
- 계층 간 조율(Orchestration)역할 구분예시성격
비즈니스 로직 이메일 중복 체크, 배송 상태에 따른 취소 여부 판단 도메인 고유의 핵심 규칙 트랜잭션 관리 @Transactional 데이터 일관성 보장 (기술적 처리) DTO 변환 PostRequestDto → Post 엔티티 계층 간 데이터 이동 구조화 검증(Validation) 비밀번호 길이 검사 입력 무결성(일부는 비즈니스 로직) 캐싱·보안·로깅 @Cacheable, @PreAuthorize, AOP 로깅 애플리케이션 전반 공통 기능 외부 연동 메일·SMS·결제 API 호출 외부 시스템 통합
3. Repository Layer (저장소 계층)
- 데이터베이스 CRUD 전담
Service 계층과 Controller 계층의 책임 분리 모호함.⇒ 재사용성과 유지보수 때문 - ex) 새로 Grapqhql를 도입 하고자 할 때 이미 Controller에 모든 기능을 구현하였다면, Grapqhql API를 구현하는 Controller에 또 다시 구현해야함.

- GqlController를 새로 만들어 Service에 연결 - 왜 이렇게 분리할까?
어디에 어떤 코드를 두면 좋을까? 에 도움되는 아키텍처 패턴
서비스 레이어 유틸화(Service Layer Utilization)
- A서비스가 B, C, D… 여러 도메인을 다 써야 하는 상황
- A서비스 클래스 안에 BService, BRepository, CService, CRepository… 14개 필드가 주입된 상황
- 필요한 Service Layer의 메서드를 정적(static) 메서드로 → A서비스에는 UtilityClass 하나만 참조 하면 됨
- 만들어야 하는 Mock 객체 줄어듬
- 유연하게 대처 가능
ex) A서비스 = “‘책상’, ‘의자’, ‘컴퓨터’, ‘프린터’ … 14가지” 다 갖고 일해야 하는 사람
A서비스 = “사무도우미(UtilityClass) 한 명”만 옆에 두고 필요한 일은 그 사람이 다 해줘서
A서비스는 “프린터 하나만 딱 쓰면” 되는 상황
CQRS (Command and Query Responsibility Segregation)
- 명령(Command, 쓰기)” 과 “조회(Query, 읽기)” 를 완전히 분리해서 다루는 아키텍처 패턴
- Command : 객체를 변경하는 쓰기 (읽기는 빠른 조회)
- Query : 데이터 조회 기능만을 가진 함수 (쓰기는 정합성, 복잡한 비즈니스 규칙)
- 전통적인 CRUD
POST /orders → 주문 생성
GET /orders → 주문 목록 조회
PUT /orders/1 → 주문 수정
- CQRS 적용
- 쓰기(Commands)
- POST /orders → OrderWriteService.createOrder(...)
- 내부에서 order_write_table 에 INSERT
- 읽기(Queries)
- GET /orders → OrderReadService.findAllOrders()
- order_read_table 또는 캐시/검색엔진에서 SELECT
Service 레이어 내부를 쓰기/읽기용 두 클래스로 나눈 예제
// Command 서비스 (트랜잭션, 검증)
@Service
public class OrderWriteService {
@Transactional
public Long createOrder(CreateOrderCommand cmd) {
// 검증, 엔티티 생성, 저장
}
}
// Query 서비스 (빠른 조회)
@Service
public class OrderReadService {
public List<OrderViewDto> findAllOrders() {
// 최적화된 뷰 테이블/캐시에서 가져오기
}
}
명령(Command) DTO 와 조회(Query) DTO 를 완전히 분리한 예제
// Command DTO
public record CreatePostCmd(String title, String content) { }
// Query DTO
public record PostOverview(Long id, String title, LocalDateTime createdAt) { }
// 쓰기 컨트롤러
@PostMapping
public long create(@RequestBody CreatePostCmd cmd) {
return postCommandService.create(cmd);
}
// 조회 컨트롤러
@GetMapping("/overview")
public List<PostOverview> overview() {
return postQueryService.listOverview();
}
ex)
- Command 창구: “주문 받기” 전담, 메뉴 옵션 검증·주문표 생성
- Query 창구: “주문 현황 조회” 전담, 손님에게 빠르게 현재 대기·조리 상태 제공
2. DTO를 활용한 데이터 전달 패턴 예시 정리하기
- DTO / DAO / VO 차이점
DTO(Data Transfer Object)

DAO(Data Access Object)
- DB에 접근하는 역할

VO(Value Object)
- 변경 불가능한 객체
- 일반적으로 날짜, 통화 또는 수량을 나타내는 데 사용
엔티티와 DTO 차이
- DTO(Data Transfer Object) : 클라이언트와 서버 간 데이터 전송을 전송을 위한 객체
- Entity : 데이터베이스에 저장되는 데이터 객체로, 데이터베이스와 직접적으로 매핑되는 객체
Request ⇄ Response DTO 분리
- Request DTO : 클라이언트가 보내는(요청)
- Response DTO : 서버가 돌려주는(응답)
// 글 작성 요청 DTO
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class AddArticleRequest {
private String title;
private String content;
public Article toEntity() {
return Article.builder()
.title(title)
.content(content)
.build();
}
}
// 글 작성 응답 DTO
@Getter
public class ArticleResponse {
private final String title;
private final String content;
public ArticleResponse(Article article) {
this.title = article.getTitle();
this.content = article.getContent();
}
}
Mapper/Assembler 분리
Entity ↔ DTO 변환 로직을 한곳에 모아 재사용·테스트 용이
@Component
public class PostMapper {
public Post toEntity(AddPostRequest req) {
return Post.builder()
.title(req.getTitle())
.content(req.getContent())
.build();
}
public PostDto toDto(Post post) {
return new PostDto(post.getId(), post.getTitle(), post.getContent());
}
}
컬렉션 DTO
글 목록 + 전체 개수·페이지 정보 같은 메타데이터를 함께 전달
@Getter
public class ArticleListViewResponse {
private final Long id;
private final String title;
private final String content;
public ArticleListViewResponse(Article article) {
this.id = article.getId();
this.title = article.getTitle();
this.content = article.getContent();
}
}
중첩(Nested) DTO
글과 그에 달린 댓글을 한 번에 내려줄 때
화면(View) 전용 DTO (ViewModel)
Thymeleaf 같은 템플릿에서 딱 필요한 데이터만 전달
@Controller //컨트롤러 표시 -> 컴포넌트 스캔을 통해 빈으로 등록
public class ExampleController {
//컨트롤러 클래스 선언
@GetMapping("/thymeleaf/example") // 이 URL로 GET 요청이 오면, thymeleafExample() 호출
public String thymeleafExample(Model model) { //Model 객체를 통해 데이터를 전달 (뷰로 전달)
Person examplePerson = new Person(); // 컨트롤러 내부 DTO 역할을 할 Person 객체 생성
examplePerson.setId(1L);
examplePerson.setName("홍길동");
examplePerson.setAge(11);
examplePerson.setHobbies(List.of("운동", "독서"));
model.addAttribute("person", examplePerson); // 뷰에 Person 객체 전달
model.addAttribute("today", LocalDate.now()); // today key로 날짜를 뷰에 전달
return "example"; // example.html로 이동
// src/main/resources/templates/example.html)을 찾아 렌더링
}
@Setter
@Getter
class Person { // Person 클래스 정의 == 내부 DTO 역할
private Long id;
private String name;
private int age;
private List<String> hobbies;
// 단순 데이터 전달용 -> 엔티티 사용 X
}
}
Projection 기반 DTO (JPA 인터페이스 프로젝션)
DB 조회 시 특정 컬럼만 바로 뽑아와 SQL/PERFORMANCE 최적화
3. 간단한 비즈니스 로직 구상해오기 (ex: 회원가입, 게시글 작성)
도서 대출 기능
- 사용자가 대출을 원하는 도서명을 입력한다.
- 입력된 도서명으로 데이터베이스에서 해당 도서명을 가진 모든 책을 검색한다.
- 검색된 책 목록을 사용자에게 보여준다.
- 각 책의 ID, 제목, 대출 가능 여부를 포함한다.
- 사용자가 대출을 원하는 책의 ID를 선택한다.
- 선택된 책의 대출 가능 여부를 확인한다.
- 대출 가능 여부가 false인 경우, 대출 불가능하다는 메시지를 출력한다.
- 대출 가능 여부가 true인 경우:
a. 해당 책의 대출 상태를 false로 업데이트한다.
b. 데이터베이스에 변경 사항을 저장한다.
c. 대출 성공 메시지를 출력한다.
💡 새롭게 알게 된 점 & 어려웠던 부분
✔️ DTO의 개념을 다시한번 복습하게 됨
✔️ 데이터 전달 패턴이 다양해서, 이해하는데 시간이 걸렸음
🚀 과제/실습 결과 (있을 시)
도서 대출 로직
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import me.nam.springbootdeveloper.Domain.Book;
import me.nam.springbootdeveloper.Repository.BookRepository;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class BookService {
private final BookRepository bookRepository;
public List<BookResponse> searchBooksByTitle(String title) {
List<Book> books = bookRepository.findAllByTitle(title);
return books.stream()
.map(BookResponse::new)
.toList();
}
@Transactional
public void borrowingBook(Long id) {
Book book = bookRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 ID의 책을 찾을 수 없습니다: " + id));
if (!book.isAvailable()) {
throw new IllegalStateException("해당 책은 대출이 불가능합니다: " + book.getTitle());
}
book.setAvailable(false);
bookRepository.save(book);
}
}
👥 스터디 피드백
✔️서비스 로직에 예외 처리를 두는 이유
도메인 규칙(예: 중복 회원 검증)을 처리하는 역할
컨트롤러에서 하면 안 되는 이유
계층 간 역할이 모호,코드 재사용성이 떨어지고, 테스트가 어려워짐
✔️공통 에러 처리: 예외를 한 곳에서 처리
@ControllerAdvice:모든 컨트롤러에서 발생하는 예외를 처리하는 클래스
@ExceptionHandler: 특정 예외를 처리하는 메서드를 정의
✔️스파게티 코드 -> 리팩토링