[Spring]스프링 계층 구조 - Controller, Servcie, Repository, DAO, DTO, Entity
★ Spring 웹 어플리케이션 계층 구조
다음 그림은 스프링의 계층 구조를 전체적으로 나타낸 것입니다.
스프링의 웹 계층은 다음 4가지 계층으로 나뉩니다.
- Domain Model
- Presentation Model(Controller)
- Business Layer(Service Layer)
- Data Access Layer(Repository Layer)
★ Domain Model
도메인 모델 Domain Model(Object) 는 개발하고자 하는 영역을 분석하고, 그 분석의 결과로 도출된 모델(객체)이라고 할 수 있습니다.
도메인 모델은 특정 도메인을 개념적으로 표현한 것으로 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화한 것입니다.
관계형 데이터베이스의 Entity 와 비슷한 개념을 가지며, @Entity 어노테이션이 사용된 영역을 도메인이라고 할 수 있습니다.
또한 VO 또는 DTO 와 같은 값 객체들도 이 영역에 해당됩니다.
도메인 모델 클래스는 나머지 3개의 계층 전체에 걸쳐 사용되며, private 로 선언된 멤버 변수와 Getter/Setter 메서드를 가집니다.
이는 DB의 테이블과 매칭되며 Entity 클래스 또는 가장 Core한 클래스라고 불립니다.
이때 도메인 모델은 오직 Domain 로직만을 가지고 있어야 하며, Presentation 로직을 가지고 있어서는 안됩니다.
PostEntity.java
package com.dev.demojpa.entity;
import javax.persistence.*;
@Entity
@Table(name = "post")
public class PostEntity extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
private String writer;
@ManyToOne(
targetEntity = BoardEntity.class,
fetch = FetchType.LAZY
)
@JoinColumn(name = "board_id")
private BoardEntity boardEntity;
public PostEntity() {
}
public PostEntity(Long id, String title, String content, String writer, BoardEntity boardEntity) {
this.id = id;
this.title = title;
this.content = content;
this.writer = writer;
this.boardEntity = boardEntity;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getWriter() {
return writer;
}
public void setWriter(String writer) {
this.writer = writer;
}
public BoardEntity getBoardEntity() {
return boardEntity;
}
public void setBoardEntity(BoardEntity boardEntity) {
this.boardEntity = boardEntity;
}
}
PostDto.java
package com.dev.demojpa.dto;
public class PostDto {
private int id;
private String title;
private String content;
private String writer;
private int boardId;
public PostDto() {
}
public PostDto(int id, String title, String content, String writer, int boardId) {
this.id = id;
this.title = title;
this.content = content;
this.writer = writer;
this.boardId = boardId;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getWriter() {
return writer;
}
public void setWriter(String writer) {
this.writer = writer;
}
public int getBoardId() {
return boardId;
}
public void setBoardId(int boardId) {
this.boardId = boardId;
}
}
★ Presentation Layer - Controller
프레젠테이션 계층은 클라이언트로부터 HTTP 요청을 수신하고, 이에 해당하는 응답을 주는 계층입니다.
어떤 요청을 받고, 어떤 응답 데이터를 반환하는지에 대한 책임을 가지고 있으며 URI를 어떤 방식으로 처리할 것인지에 대한 설계가 필요합니다.
@Controller, @RestController, @GetMapping, @PostMapping 과 같은 어노테이션과 함께 사용됩니다.
클라이언트의 요청과 응답을 처리하는 과정은 다음과 같습니다.
클라이언트로부터 요청을 DTO 의 형태로 받아 Service 기능을 호출하고, 적절한 응답을 DTO 의 형태로 반환합니다.
PostController.java
package com.dev.demojpa.controller;
import com.dev.demojpa.dto.PostDto;
import com.dev.demojpa.service.PostService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("post")
public class PostController {
private final PostService postService;
// DI: Controller 가 Servcie 를 의존하는 관계
public PostController(
@Autowired PostService postService
) {
this.postService = postService;
}
@PostMapping("")
public void createPost(@RequestBody PostDto dto) {
this.postService.createPost(dto);
}
@GetMapping("{id}")
public PostDto readPost(@PathVariable("id") int id) {
return this.postService.readPost(id);
}
}
★ Business Layer - Service
비지니스 계층은 요구사항에 맞게 비지니스 로직을 작성하고 해결하는 계층으로, 서비스의 핵심 로직들이 주로 포함되어 있습니다.
@Service, @Transactional 어노테이션과 함께 사용되며, 어플리케이션 비지니스 로직 처리와 비지니스와 관련된 도메인 모델의 적합성 검증 또는 트랜잭션을 관리합니다.
Presentation 계층과 Data Access 계층 사이를 연결하는 역할로서 두 계층이 직접적으로 통신하지 않게 합니다.
즉, DTO를 통해 받은 데이터를 이용해 비지니스 로직을 처리하고, DAO(Repository)를 통해 DB에 접근하여 데이터를 관리합니다.
Controller가 Service에 의존하고, Service는 Repository(DAO)에 의존하는 관계를 이룹니다.
트랜잭션 Transaction
: DB 상태를 변화시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위 또는 한꺼번에 모두 수행되어야 할 일련의 연산들
트랜잭션 ACID
- Atomicity 원자성: 트랜잭션 내의 작업들은 모두 성공 또는 모두 실패한다.
- Consistency 일관성: 모든 트랜잭션은 일관성 있는 DB 상태를 유지한다.
- Isolation 격리성: 동시에 실행되는 트랜잭션들은 서로 영향을 미치지 않는다.
- Durability 지속성: 트랜잭션이 성공적으로 끝나면, 그 결과는 항상 기록되어야 한다.
PostServcie.java
package com.dev.demojpa.service;
import com.dev.demojpa.dao.PostDao;
import com.dev.demojpa.dto.PostDto;
import com.dev.demojpa.entity.PostEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@Service
public class PostService {
private static final Logger logger = LoggerFactory.getLogger(PostService.class);
private final PostDao postDao;
public PostService(
@Autowired PostDao postDao
) {
this.postDao = postDao;
}
public void createPost(PostDto dto) {
this.postDao.createPost(dto);
}
public PostDto readPost(int id) {
PostEntity postEntity = this.postDao.readPost(id);
// entity -> dto 로 변환
PostDto postDto = new PostDto(
Math.toIntExact(postEntity.getId()),
postEntity.getTitle(),
postEntity.getContent(),
postEntity.getWriter(),
postEntity.getBoardEntity() == null ?
0 : Math.toIntExact(postEntity.getBoardEntity().getId())
);
return postDto;
}
}
★ Data Access Layer - Repository(DAO)
데이터 엑세스 계층은 데이터를 저장하거나 조회하는(CRUD) 등을 어떻게 할 것인지를 정의하여 DB 에 접근하는 계층입니다.
@Repository 어노테이션을 사용하여 Bean 으로 등록하며, 에러 추상화 등 부가적인 기능을 제공받을 수 있습니다.
DAO 인터페이스와 DAO 구현 클래스(@Repository 를 사용하는 클래스)가 이 계층에 속합니다.
JPA 와 ORM(Hibernate) 를 주로 사용하는 계층으로, JPA 를 사용하면 Repository 를 이용하여 DB에 실제로 접근할 수 있습니다.
Service 와 DB 를 연결해주는 역할을 하며, Service 계층에서 Repository 를 이용하여 데이터를 관리할 수 있습니다.
PostDao.java
package com.dev.demojpa.dao;
import com.dev.demojpa.dto.PostDto;
import com.dev.demojpa.entity.PostEntity;
import com.dev.demojpa.repository.PostRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Repository;
import org.springframework.web.server.ResponseStatusException;
import java.util.Iterator;
import java.util.Optional;
@Repository
public class PostDao {
private static final Logger logger = LoggerFactory.getLogger(PostDao.class);
private final PostRepository postRepository;
public PostDao(
@Autowired PostRepository postRepository
) {
this.postRepository = postRepository;
}
public void createPost(PostDto dto) {
PostEntity postEntity = new PostEntity();
postEntity.setTitle(dto.getTitle());
postEntity.setContent(dto.getContent());
postEntity.setWriter(dto.getWriter());
postEntity.setBoardEntity(null);
this.postRepository.save(postEntity);
}
public PostEntity readPost(int id) {
Optional<PostEntity> postEntity = this.postRepository.findById((long) id);
if(postEntity.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
return postEntity.get();
}
}
PostRepository.java
package com.dev.demojpa.repository;
import com.dev.demojpa.entity.BoardEntity;
import com.dev.demojpa.entity.PostEntity;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
public interface PostRepository extends CrudRepository<PostEntity, Long> {
}
★ Entity
Entity 는 DB 테이블에 존재하는 Column 들을 필드로 가지는 객체를 의미합니다. DB 의 테이블과 1 대 1 대응이며, 테이블에 가지지 않는 칼럼을 필드로 가셔는 안됩니다.
Entity 의 가장 큰 특징은 식별자(id)를 가진다는 것입니다.
식별자 이외에 데이터가 변경 되어도 해당 객체가 다른 객체가 되는 것이 아닙니다.
Entity 클래스는 @Entity, @Id, @Column 등과 같은 어노테이션과 함께 사용되며, 다른 클래스를 상속받거나 인터페이스의 구현체여서는 안되고 순수한 데이터 객체인 것이 좋습니다.
(단, createdAt, updatedAt 과 같은 공통적인 칼럼에 대헤서는 BaseEntity 를 만들어 상속 가능)
★ DTO
DTO 는 Data Transfer Object 의 약자로 데이터를 Transfer(이동) 하기 위한 객체입니다. Domain 또는 VO(Value Object) 라고도 부릅니다.
DTO 는 로직을 갖고 있지 않은 순수한 데이터 객체로, 일반적으로 Getter/Setter 메서드만을 가집니다.
(사실 DB에서 꺼낸 값을 임의로 변경할 필요가 없으므로 Setter 는 필요가 없습니다. 대신 생성자에서 값을 할당하는 것이 좋습니다.)
클라이언트와 Controller 가 요청/응답을 주고 받을 때 DTO 의 형태로 데이터가 이동하며, DB 에서 데이터를 얻어 Service, Controller 등으로 보낼 때 사용됩니다.
Entity 객체를 그대로 사용하지 않고 DTO 를 사용하는 이유
1. 객체를 표현하기 위한 계층(View Layer)과 저장하는 계층(DB Layer)의 역할을 분리하기 위해
2. 프로그래머의 의도와는 상관없는 Entity 객체의 변경을 피하기 위해
3. 요구사항이 변경됨에 따라 View 와 통신하는 DTO 클래스는 자주 변경되기 때문에
4. Entity 의 도메인 모델링을 지키기 위해
★ DAO
DAO 는 Data Access Object 의 약자로 실제 DB 에 접근하는 객체를 의미합니다.
Service 와 실제 DB 를 연결하는 역할을 담당하는 즉, DB 에서 데이터를 가져오거나 저장하는 역할을 담당합니다.