카테고리 없음

백엔드 스터디 후기

myhousemouse 2026. 4. 30. 11:01

📌 기본 정보

  • 주차/주제: 6주차 - 비즈니스 로직 구현과 서비스 레이어

📚 학습 내용 요약

1. Service 레이어의 역할과 책임 조사하기

  • Layer는 관심사의 집합

1. Presentation Layer (프레젠테이션 계층)

  • 사용자(UI) 요청 받기: 웹브라우저·앱·윈도우 애플리케이션 등 어디서든 들어오는 요청을 처리
  • 응답(View) 반환: 처리 결과를 HTML 페이지나 JSON 등으로 포장해 사용자에게 돌려줌
  • 입력 검증·예외 처리: URL 잘못 호출, 필수 파라미터 누락 같은 “화면 관점” 오류를 잡아서 사용자에게 친절한 메시지 제공

2. Service Layer (서비스 계층)

  • 순수 비즈니스 로직 구현
  • 트랜잭션 경계 관리 (@Transactional)
    • 모두 성공시키거나, 모두 취소(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를 활용한 데이터 전달 패턴 예시 정리하기

  1. 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: 회원가입, 게시글 작성)

도서 대출 기능

  1. 사용자가 대출을 원하는 도서명을 입력한다.
  2. 입력된 도서명으로 데이터베이스에서 해당 도서명을 가진 모든 책을 검색한다.
  3. 검색된 책 목록을 사용자에게 보여준다.
    • 각 책의 ID, 제목, 대출 가능 여부를 포함한다.
  4. 사용자가 대출을 원하는 책의 ID를 선택한다.
  5. 선택된 책의 대출 가능 여부를 확인한다.
    • 대출 가능 여부가 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: 특정 예외를 처리하는 메서드를 정의

✔️스파게티 코드 -> 리팩토링