출처 : 이동욱 개발자님 [스프링 부트와 AWS로 혼자 구현하는 웹 서비스]
내가 보려고 쓰는 Spring 프로젝트 구조
모든 응답 Dto는 DTO 패키지에 추가
등록 / 수정 / 조회 API 만들기
API 를 만들기 위해 총 3개의 클래스가 필요하다.
- Request 데이터를 받을 Dto
- API 요청을 받을 Controller
- 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service
🚨 오해
Servuce에서 비즈니스 로직을 처리해야 한다.
하지만, 전혀 그렇지 않다.
그럼?
💡 Service는 트랜잭션, 도메인 간 순서 보장의 역할만 한다.
그럼 비즈니스 로직은 누가 처리하나?
간단히 각 영역을 소개하자면 다음과 같다.
Web Layer |
흔히 사용하는 컨트롤러(@Controller)와 JSP / Freemarker 등의 뷰 템플릿 영역이다. |
이외에도 필터(@Filter), 인터셉터, 컨트롤러 어드바이스(@ControllerAdvice) 등 외부 요청과 응답에 대한 전반적인 영역을 이야기한다. |
Service Layer |
@Service에 사용되는 서비스 영역이다. |
일반적으로 Controller와 Dao의 중간 영역에서 사용된다. |
@Transactional이 사용되어야 하는 영역이기도 하다. |
Repository Layer |
Database와 같이 데이터 저장소에 접근하는 영역이다. |
Dao(Data Access Object) 영역으로 이해하면 쉽다. |
Dtos |
Dto(Data Transfer Object)는 계층 간에 데이터 교환을 위한 객체이다. |
예를 들어, 뷰 템플릿 엔진에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등이 이들을 이야기한다. |
Damain Model |
도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화 시킨 것 |
이를테면 택시 앱이라고 하면 배차, 탑승, 요금 등이 모두 도메인이 될 수 있다. |
@Entity가 사용된 영역 역시 도메인 모델이라고 이해하면 된다. |
다만, 무조건 데이터베이스의 테이블과 관계가 있어야만 하는 것은 아니다. |
VO처럼 값 객체들도 이 영역에 해당하기 때문이다. |
Web(Controller), Service, Repository, Dto, Domain,
이 5가지 레이어에서 비지니스 처리를 담당해야 할 곳은 어딜까?
→ 💡 바로 Domain이다!
만약, 기존에 서비스로 처리하던 방식으로 한다면?
모든 로직이 서비스 클래스 내부에서 처리 된다면?
→ 서비스 계층이 무의미하며,
객체란 단순히 데이터 덩어리 역할만 하게 된다.
반면, 도메인 모델에서 처리할 경우
@Transactional
public Order cancelOrder(int orderId) {
// 1)
Order order = orderRepository.findById(orderId);
Billing billing = billingRepository.findbyOrderId(orderId);
Delivery delivery = deliveryRepository.findByOrderId(orderId);
// 2 ~ 3)
delivery.cancel();
// 4)
order.cancel();
billing.cancel();
return order;
}
/**
* 1) 데이터베이스로부터
* 주문정보(Orders), 결제정보(Billing), 배송정보(Delivery) 조회
*
* 2) 배송 취소를 해야 하는지 확인
*
* 3) 배송 중이라면, 배송 취소로 변경
*
* 4) 각 테이블에 취소 상태 Update
*/
order, billing, delivery가 각자 본인의 취소 이벤트 처리를 하며,
서비스 메소드는 트랜잭션과 도메인 간의 순서만 보장해 준다.
[PostsApiContoller.java]
@RequiredArgsConstructor
@RestController
public class PostsApiController {
private final PostsService postsService;
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto) {
return postsService.save(requestDto);
}
}
💡 @Autowired가 없는 이유?
스프링에선 Bean을 주입받는 방식들은 다음과 같다.
1. @Autowired
2. setter
3. 생성자
이 중 가장 권장하는 방식이 생성자로 주입받는 방식이다.
(@Autowired는 권장하지 않음)
즉, 생성자로 Bean 객체를 받도록 하면 @Autowired와 동일한 효과를 볼 수 있다.
그럼 생성자는 어디 있을까?
💡 @RequiredArgsConstructor에서 해결해 준다.
→ final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성해준다.
[PostsService.java]
@RequiredArgsConstructor
@Service
public class PostsService {
private final PostsRepository postsRepository;
@Transactional
public Long save(PostsSaveRequestDto requestDto) {
return postsRepository.save(requestDto.toEntity()).getId();
}
}
이제는 Controller와 Service에서 사용할 Dto 클래스를 생성해보자.
[PostsSaveRequestDto.java]
@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
private String title;
private String content;
private String author;
@Builder
public PostsSaveRequestDto(String title, String content, String author) {
this.title = title;
this.content = content;
this.author = author;
}
public Posts toEntity() {
return Posts.builder()
.title(title)
.content(content)
.author(author)
.build();
}
}
💡 여기서 Entity 클래스와 거의 유사한 형태임에도
Dto 클래스를 추가로 생성했다.
하지만, 절대로 Entity 클래스를 Request / Response 클래스로 사용해서는 안 된다.
Entity 클래스가 변경되면 여러 클래스에 영향을 끼치지만,
Request와 Response용 Dto는 View를 위한 클래스라 자주 변경이 필요하다.
View Layer와 DB Layer의 역할 분리를 철저하게 하는 게 좋다.
실제로 Controller에서 결과값으로 여러 테이블을 조인해서 줘야 할 경우가 빈번하다.
꼭 [Entity 클래스]와 [Controller에서 쓸 Dto]는 분리해서 사용해야 한다.
※ 참고 [Posts.java]
@Getter
@NoArgsConstructor
@Entity
public class Posts {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 500, nullable = false)
private String title;
@Column(columnDefinition = "Text", nullable = false)
private String content;
private String author;
@Builder
public Posts(String title, String content, String author) {
this.title = title;
this.content = content;
this.author = author;
}
}
등록 기능을 완성했으니 수정 / 조회 기능을 만들어보자.
수정
[PostsApiController.java]
@RequiredArgsConstructor
@RestController
public class PostsApiController {
// ...
@PutMapping("/api/v1/posts/{id}")
public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto) {
return postsService.update(id, requestDto);
}
}
[PostsUpadteRequestDto.java]
@Getter
@NoArgsConstructor
public class PostsUpdateRequestDto {
private String title;
private String content;
@Builder
public PostsUpdateRequestDto(String title, String content) {
this.title = title;
this.content = content;
}
}
[Posts.java]
public class Posts {
// ...
public void update(String title, String content) {
this.title = title;
this.content = content;
}
}
[PostsService.java]
@RequiredArgsConstructor
@Service
public class PostsService {
// ...
@Transactional
public Long update(Long id, PostsUpdateRequestDto requestDto) {
Posts posts = postsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id = " + id));
posts.update(requestDto.getTitle(), requestDto.getContent());
return id;
}
}
💡 update 기능에서 데이터베이스에 쿼리를 날리는 부분이 없다.
이게 가능한 이유는 JPA의 영속성 컨텍스트 때문이다.
※ 영속성 컨텍스트란?
→ 엔티티를 영구 저장하는 환경이다.
JPA의 핵심 내용은
엔티티가 영속성 컨텍스트에 포함되어 있냐 아니냐로 갈린다.
JPA의 엔티티 매니저가 활성화된 상태로(Spring Data Jpa를 쓴다면 기본 옵션)
트랜잭션 안에서 데이터베이스에 데이터를 가져오면
이 데이터는 영속성 컨텍스트가 유지된 상태이다.
이 상태에서 해당 데이터의 값을 변경하면
트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영한다.
즉, Entity 객체의 값만 변경하면
별도로 Update 쿼리를 날릴 필요가 없다.
이 개념을 더티 체킹이라고 한다.
💡 더티 체킹 (Dirty Checking)이란?
→ Transaction 안에서 엔티티의 변경이 일어나면,
변경 내용을 자동으로 데이터베이스에 반영하는 JPA의 특징이다.
변경 감지해서 DB에 반영한다.
※ 데이터베이스에 변경 데이터를 저장하는 시점
1. Transaction Commit
2. EntityManager Flush
3. JPQL 사용
JPA에서는 트랜잭션이 끝나는 시점에
변화가 있는 모든 엔티티 객체를 데이터베이스에 자동으로 반영해준다.
출처 : https://velog.io/@jiny/JPA-%EB%8D%94%ED%8B%B0-%EC%B2%B4%ED%82%B9Dirty-Checking-%EC%9D%B4%EB%9E%80
조회
[PostsApiController.java]
@RequiredArgsConstructor
@RestController
public class PostsApiController {
// ...
@GetMapping("/api/v1/posts/{id}")
public PostsResponseDto findById (@PathVariable Long id) {
return postsService.findById(id);
}
}
[PostsResponseDto.java]
@Getter
public class PostsResponseDto {
private Long id;
private String title;
private String content;
private String author;
public PostsResponseDto(Posts entity) {
this.id = entity.getId();
this.title = entity.getTitle();
this.content = entity.getContent();
this.author = entity.getAuthor();
}
}
PostsResponseDto는 Entity의 필드 중 일부만 사용하므로
생성자로 Entity를 받아 필드에 값을 넣는다.
굳이 모든 필드를 가진 생성자가 필요하진 않으므로
Dto는 Entity를 받아 처리한다.
[PostsService.java]
@RequiredArgsConstructor
@Service
public class PostsService {
// ...
public PostsResponseDto findById(Long id) {
Posts entity = postsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id = " + id));
return new PostsResponseDto(entity);
}
}
'Spring > 그 외' 카테고리의 다른 글
[Spring] 왜 스프링을 쓰는가? (특징 & 계층 구조) (0) | 2023.01.23 |
---|---|
Spring(스프링) 주요 어노테이션 정리 (0) | 2023.01.22 |
domain, JpaRepository (0) | 2023.01.07 |
스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 (0) | 2023.01.04 |
혼자 구현하는 웹 서비스 (0) | 2023.01.03 |