작업 일지#2 상품 도메인에 기능을 - repository, service, controller
개요
작업일지#1 도메인을 짜보자 - 상품 도메인 설계를 기반으로 설계한 Item entity를 바탕으로 기본적인 CRUD 비즈니스 로직을 구현하기로 했다. 여기서 CRUD는 Create, Read, Update, Delete 이 네 단어의 앞글자만 붙인 용어로, 데이터의 기본 성질을 표현할 때 많이 사용하는 단어이고, entity는 도메인 주도 설계에서 ID를 통해 객체를 구분하며, mutable한 튿징을 가진 plain object이다.
'토비의 스프링'에 따르면, 스프링은 다음과 같은 3계층 아키텍처에 적합한 프레임워크라고 한다.
- 프레젠테이션
- 웹, UI (controller)
- 서비스
- 비즈니스 로직 (Service)
- 데이터 액세스
- DAO 계층 (Repository)
이번 프로젝트에는 이 계층에 완전히 맞게 설계하지는 않았지만, 역할에 따라 객체와 로직을 분류를 하여 객체지향적인 코드를 갖기 위해서는 위의 3계층의 특징을 어느정도 파악하는 것이 중요하다고 생각했다. 뒤에서 자세히 서술하겠지만, 상품 도메인에서 진행한 로직의 흐름은 거의 동일했다. - handler -> Service -> Repository*
- DAO 계층 (Repository)
Repository vs DAO
- StackOverflow에서 이 둘의 차이를 명확하게 설명한 글이 있다. What is the difference between DAO and Repository patterns?
개인적으로는 이 게시글 만큼 Repository 패턴과 DAO 패턴의 차이를 명확하게 설명한 글은 없다고 생각했는데 내용을 요약해보자면.. DAO
는 data persistent를 추상화한 패턴이다.Repository
는 오브젝트 콜렉션을 추상화한 패턴이다.
사실 기능적으로 봤을 때는 DAO를 사용하든, Repository를 사용하든, 결국에 데이터 액세스 로직을 수행하는 것은 마찬가지지만, 나는 데이터 자체보다는 '객체'의 관점에서 로직을 다루고 싶었기 때문에, Repository 패턴을 채택했다.
Repository, Service
SQL Mapper & Repository
구현CRUD를 통해 비즈니스 로직을 구현하기 위해선 우선 데이터 액세스 즉, DB와 같은 DataSource에 접근할 수 있는 로직이 필요하다. Java에는 DataSource에 접근할 수 있는 JDBC API를 제공한다. 그런데, 보통 이 API를 그대로 사용하면, 코드가 길어지고, 핵심 로직을 구현하는데 까지 필요한 불필요한 준비과정이 많아서, 보통은 JPA나, MyBatis와 같은 ORM이나, SQL mapper를 통해, 좀더 쉽게 데이터 액세스 로직을 구현한다. 이번 프로젝트의 경우는 MyBatis를 활용하게 되었는데, MyBatis를 Spring Boot와 같이 활용할 경우 SQL을 매핑하는 다양한 방법을 제공받게 된다. 하지만 나는SQL 구문 작성과 데이터 액세스 로직을 최대한 분리하는 방식을 통해, 반복된 SQL 구문 작성을 피하고 싶었기 때문에 @Mapper
와 mapper.xml
을 분리하여 관리하기로 했다. 이 방식을 코드로 표현해보면 다음과 같을 것 같다.
- 데이터 액세스 로직
매핑된 SQL문을 호출하는 mapper 클래스
@Mapper
public interface ItemMapper {
Item selectById(Long id);
...;
}
SQL을 JAVA 객체에 매핑하기 위해 작성하는 mapper.xml
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.project.doubleshop.domain.item.mapper.ItemMapper">
<select id="selectById" parameterType="long" resultType="Item">
SELECT * FROM ITEM WHERE id = #{id}
</select>
...
</mapper>
MyBatis와 Spring Boot를 연동하는 방법은 매우 다양하다. 이 글은 특정 코드의 구현 방식을 정리하기보다는 프로젝트 작업을 하면서 고민한 흔적을 남기는 것이 최우선이므로, 이 방법에 대해선 자세히 서술하진 않겠다. 자세한 구현방식은 MyBatis의 공식 문서를 참고하면 정말 좋다.
https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
몇몇 블로그에서 MyBatis와 Spring Boot를 같이 사용하는 예제를 보면, Mapper를 그대로, Service 로직에 구현하는 경우가 생각보다 많았다. 하지만 이런 방식을 사용할 경우 다음과 같은 문제점이 생길 수 있다.
- 데이터 액세스 로직이 특정 프레임워크에 종속된다.
- 특정 프레임워크에 종속되기 때문에, 확장성이 없는 구조가 되어 유지보수 효율이 좋지 못하다.
- 따라서 myBatis로 데이터 액세스 로직을 구현할 때는 Mapper를 직접 사용하는 것보다는 Repository 계열의 클래스로 캡슐화하는 것이 좋다.
public interface ItemRepository {
boolean save(Item entity);
}
@Repository
@RequiredArgsConstructor
public class MyBatisItemRepository implements ItemRepository {
private final ItemMapper mapper;
@Override
public Item findById(Long id) {
return mapper.selectById(id);
}
}
Service
데이터 액세스 로직을 구현하였으니, 이제 그 데이터를 통해 구현하는 서비스를 만들면 되겠다. 이 부분은 코드 내용부터 보여주고, 내용을 서술하도록 하겠다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
public Optional<Item> findItem(Long itemId) {
return Optional.ofNullable(itemRepository.findById(itemId));
}
}
@Transactional
은 보통 서비스 로직을 구현하기 위해 사용하는 데이터 액세스 로직 하나로 해결되는 경우는 없기 때문에, 트랜잭션 관리 기능을 통해, 하나의 서비스 로직을 구현하기 위해 필요한 작업의 단위를 구분할 필요가 있다. 이 애노테이션은 그 관리 기능을 편리하게 사용하게 해주는 Declarative Transaction management 기능인데, 이 부분은 나중에 자세히 다뤄보도록 하겠다.
지금은 물론, id pk번호를 통해 하나의 상품을 조회하는 데이터 액세스 로직 하나로 해결되니, 트랜잭션 관리 기능을 넣는 것이 무의미 하겠지만, 앞으로 더 복잡해지는 상황을 대비하기 위해 미리 관리 기능을 넣었다고 보면 되겠다.
Controller
Controller는 3계층에서 Presentation tier에 해당하는 기능을 담당하는 컴포넌트로, 보통 web과 가장 밀접한 기능을 수행한다. 주로 클라이언트의 요청에 맞는 URI path와 HTTP 메소드를 구분하여, 서비스 로직을 통해 요청에 맞는 response 데이터를 전달하는데, 이 것 역시 SSR 방식으로 View 파트를 렌더링해서 응답을 전달하느냐, 아니면, response Body를 그대로 리턴하느냐로 또 목적이 달라진다. spring에서는 이를 각각 ViewResolver
와 MessageConverter
를 통해 작업을 수행한다. 이번 프로젝트의 경우, View와 관련된 작업은 배제하고, 오직 api로 json 데이터를 전달하는 HTTP api(Restful이 되고 싶긴 하지만...)를 구현하는 것이 목적이기 때문에, 이러한 기능을 수행할 수 있는 Controller와 관련된 컴포넌트들은 @RestController
로 등록하였다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
* @since 4.0.1
*/
@AliasFor(annotation = Controller.class)
String value() default "";
}
@RestController
애노테이션을 설명하기 위해 api 내용을 가져왔는데, 우선 기존의 컨트롤러 컴포넌트 등록 애노테이션인 @Controller
와 응답 바디를 그대로 리턴하게 하는 @ResponseBody
애노테이션이 같이 선언되어 있다. 그리고 설명 내용을 요약 해보면, 이 애노테이션을 통해 핸들러를 분석하여, @RequestMapping
으로 매핑된 핸들러를 통해, http response의 body를 그대로 리턴하게 한다. 정도로 이해하면 될 것 같다. 즉 @RestController
를 통해 핸들러 메소드를 구현하면 다음과 같은 결과를 얻게 될 것이다.
public class Foo {
Integer id;
String name;
....;
}
@RestController
public class HelloController {
@GetMapping("/")
public Foo hello() {
return new Foo(1, "foo");
}
}
{
"id" : 1
"name" : "foo"
}
ResponseEntity
HTTP 메세지를 보내기 위해서는 그냥 단순히 json 데이터만 보내는 걸로 끝내선 안된다. Http status code를 통해, 해당 요청의 성공여부를 알려주어야 하고, 혹시라도 에러가 났다면, 요청이 잘못됬는지, 혹은 서버 내부에서 일어난 문제인지 알 수 있는 에러 메세지(에러가 발생했다면)도 필요하다. 따라서, 기존에 Servlet api를 그대로 사용했던 시절에는 이러한 기능들을 구현하기 위해 요청의 헤더를 일일이 불러와서 검증하고, 필요하다면 추가로 헤더를 작업하며, 응답을 출력하기 위해 여러가지 번거로운 작업들을 일일이 구현했을 것이다. (그 시절의 어려움을 겪고 지금의 개선된 프레임워크를 만들어 배포해준 선배 개발자 분들께 경의를 표합니다...)
다행히 지금은 이러한 작업을 직관적으로 수행할 수 있는 api를 제공해주고 있는데 바로 ResponseEntity
이다. 이 api를 통해서, 상품 단건 조회와 같은 api를 다음과 같이 간단하게 구현할 수 있었다.
@GetMapping("item/{id}")
public ResponseEntity<Item> findItem(@PathVariable Long id) {
Item item = itemService.findItem(id);
if(item == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(item);
}
마무리
지금까지, 상품과 관련된 CRUD 기능을 구현하면서 고민한 흔적을 정리해 보았다. 사실 예시 코드로 제시한 기능은 매우 간단한듯 하지만, 지금처럼 작성하고 끝낼 경우 다음과 같은 문제점이 있다.
- 데이터 액세스 로직에서 만약 null인 데이터를 가져오려고 하면, request가 response 되기 전에
NPE
가 발생한다. 즉 핸들러 로직은 진행이 되다가 갑자기 끊기고, 클라이언트는 오류 메세지 자체를 받을 수 없게 된다. - 예외상황에 대한 메세지도 따로 작성해야하는데, 이 구조는 핸들러 메소드의 갯수만큼 반복해서 진행할 수 밖에 없다. (코드 반복, 재사용성 x)
- 원하든 말든 기존의 item의 모든 데이터를 보여주어야 한다. (특히 사용자 데이터의 경우 민감한 데이터까지도 무분별하게 공개할 수 있다.)
다음에는 이러한 문제점을 개선한 과정을 정리하여 업로드를 하도록 하겠다.