Service Layer - Transactional
우선 Add, Update, Delete 부분을 구현하기에 앞서서, Spring Layer 에서 더 언급하고 넘어가야 할 부분이 있다.
기존에는 DAO method 에 대해서만 @Transactional annotation 을 사용하여 atomic 하게 작업이 수행되도록 하였다. 그러나, 이제는 Service Layer 의 하나의 service method 에서 여러 개의 DAO method 가 사용될 수 있다는 것을 알고 있다. 만약 하나의 DAO method 에서 오류가 발생하면 해당 service method 는 atomic 하게 수행되지 않는다는 위험성이 존재한다.
따라서, DAO method 보다 service method 에 @Transactional annotation 을 사용하여 service layer 가 transaction boundary 를 책임지도록 하는 것이 더 바람직할 것이다.
이제 본격적으로 구현을 해보자.
Update DAO
처음에는 DAO 에 대하여 구현을 시작해보자. Interface 는 다음과 같이 findById(), save(), deleteById method 에 대하여 declare 해주자.
public interface EmployeeDAO {
List<Employee> findAll();
Employee findById(int theId);
Employee save(Employee employee);
void deleteById(int theId);
}그리고 이에 대한 implementation 은 다음과 같다.
@Override
public List<Employee> findAll() {
TypedQuery<Employee> theQuery = entityManager.createQuery("from Employee", Employee.class);
return theQuery.getResultList();
}
@Override
public Employee findById(int theId) {
return entityManager.find(Employee.class, theId);
}
@Override
public Employee save(Employee theEmployee) {
return entityManager.merge(theEmployee);
}
@Override
public void deleteById(int theId) {
Employee theEmployee = entityManager.find(Employee.class, theId);
entityManager.remove(theEmployee);
}위에서 언급했듯이, DAO method 에는 @Transactional 을 설정하지 않을 것이다. 이후에 Service method 에 추가할 것이다.
Update Service
마찬가지로 Service Interface 에 DAO Interface 와 동일하게 추가한다.
public interface EmployeeService {
List<Employee> findAll();
Employee findById(int theId);
Employee save(Employee theEmployee);
void deleteById(int theId);
}그리고 이에 대한 Implementation 을 수행하자. Service method 에서는 단순하게 EmployeeDAO 를 사용하여 DAO method 를 call 하면 된다.
@Override
public List<Employee> findAll() {
return employeeDAO.findAll();
}
@Override
public Employee findById(int theId) {
return employeeDAO.findById(theId);
}
@Transactional
@Override
public Employee save(Employee theEmployee) {
return employeeDAO.save(theEmployee);
}
@Transactional
@Override
public void deleteById(int theId) {
employeeDAO.deleteById(theId);
}단순히 값을 read 만 하는 것에는 atomicity 가 필요없기 때문에 save, deleteById method 들에만 @Transactional annotation 을 추가해주면 된다.
Update Controller
Controller 를 update 하기 전에 API Design 단계에서 설계하였던 RESTful endpoint 들을 다시 확인하고, 순서대로 구현해보자.
| HTTP Method | Endpoint | CRUD Action | Impl |
|---|---|---|---|
GET | /api/employees | Read a list of employees | O |
GET | /api/employees/{employeeId} | Read a single employee | X |
POST | /api/employees | Create a new employee | X |
PUT | /api/employees | Update an existing employee | X |
DELETE | /api/employees/{employeeId} | Delete an existing employee | X |
첫 번째 Specification(명세) 에 대하여는 이미 구현이 완료되었다. 또한 위 명세에서 볼 수 있듯이, 모두 같은 endpoint 를 사용하고 있기 때문에 올바른 HTTP method 를 사용하여 이어서 다음 Specification 에 대하여 구현을 이어가보자.
Update Controller - Read a Single Employee
@GetMapping("/employees/{employeeId}")
public Employee getEmployee(@PathVariable int employeeId) {
Employee theEmployee = employeeService.findById(employeeId);
if (theEmployee == null) { throw new RuntimeException("Employee id not found - " + employeeId); }
return theEmployee;
}해당 specification 에서는 GET method 를 사용하며, employeeId 라는 path variable 을 Client 측으로부터 Controller 에 전달한다.
Update Controller - Create a New Employee
@PostMapping("/employees")
public Employee addEmployee(@RequestBody Employee theEmployee) {
theEmployee.setId(0);
return employeeService.save(theEmployee);
}여기서 setId(0) 을 해주는 특별한 이유가 있다. save() 의 내부 구현인 entityManager.merge() method 는 id 가 0 이나 null 일 경우에는 새로운 data 로 판단하여 INSERT query 가 실행되는 반면에, id 가 다른 값이라면 기존 DB 에 존재하는 data 라고 판단하여 UPDATE query 가 실행된다.
초기에 DB 를 설정할 때, id column 에 대하여는 자동 생성되도록 설정했었다. 하지만 만약 Client 가 임의로 request message 에 임의의 id 값을 추가하여 기존 DB 의 row 가 update 될 수 있는 위험성이 존재하기 때문에 id 값을 explicit 하게 0 으로 설정해주는 codeline 이 필요한 것이다.
또한 specification 에서는 POST method 를 사용한다. 그리고 addEmployee method 에서 사용되는 parameter 는 @RequestBody, 즉 request message 의 body 에 존재하는 data 이다.
이는 JSON 형식의 data 이기 때문에 Controller 가 해당 data 를 JSON format 으로 정상적인 parsing 을 하기 위하여는 HTTP request header 의 Content-type(MIME) variable 을 applicaion/json 으로 설정해주어야 한다.
Postman 에서는 위와 같이 [Body] > [raw] > [JSON] 으로 설정해주면, Postman 이 알아서 Header Variable 을 applicaion/json 으로 설정해줄 것이다. 그리고 HTTP method 를 POST 로 바꾸고, firstName, lastName, email 값을 설정하여 JSON 형식으로 [body] 에 작성하여 request 를 보내면 위와 같이 성공적으로 add 된 object data 를 확인할 수 있다.

실제 Database 에서도 잘 add 가 된 결과를 볼 수 있다.
Update Controller - Update an Existing Employee (PUT)
@PutMapping("/employees")
public Employee updateEmployee(@RequestBody Employee theEmployee) {
return employeeService.save(theEmployee);
}save method 는 위에서 봤듯이 entityManager.merge method 를 사용하기 때문에 id 가 0 이 아닐 경우에는 UPDATE query 가 실행된다. test 를 해보자.
마찬가지로 Postman 에서 위와 같이 설정을 하고, request 를 보내면 성공적으로 update 된 Database row 의 data 가 response 된 것을 볼 수 있다.
Update Controller - Update an Existing Employee (PATCH)
기존에 존재하는 entity 를 update 하는 방법은 PUT method 를 사용하는 것 외에 PATCH method 를 사용하는 방법도 있다. PUT method 는 request 를 통해 전달된 data 전체를 기존 data 에 덮어쓰기하는 방식이라면, PATCH 는 부분적으로, 예를 들면 column 하나의 값만을 바꾸는 식으로 update 가 가능하다.
@PatchMapping("/employees/{employeeId}")
public Employee patchEmployee(@PathVariable int employeeId, @RequestBody Map<String, Object> patchPayload) {
Employee tempEmployee = employeeService.findById(employeeId);
if (tempEmployee == null) { throw new RuntimeException("Employee id not found - " + employeeId); }
if (patchPayload.containsKey("id")) { throw new RuntimeException("Employee id not allowed in request body - " + employeeId); }
Employee patchedEmployee = apply(patchPayload, tempEmployee);
return employeeService.save(patchedEmployee);
}우선 method 는 위와 같이 작성한다.
PUT method 와는 달리 partial update 를 실행하므로, request body 에 { "firstName": "Lucvs" } 와 같은 식으로 부분적인 JSON 만 전달한다. 그리고 이를 @RequestBody annotation 을 통하여 body 에 있는 data 를 Map<String, Object> 의 type 으로 받아온다.
method 의 전체적인 흐름은 다음과 같다. 먼저 findById method 를 통하여 path variable 로 받아온 employeeId 에 해당하는 employee 를 가져오고, 만족하는 employee 가 없다면 RuntimeException 을 throw 한다.
이후 patchPayload, 즉 request body 로 넘어온 data 에 id field 가 포함되어 있는지 검사한다. id 가 포함되었을 때의 위험성은 위에서 다루었다. 마지막으로 tempEmployee 에 patchPayload 를 합치는 작업을 수행한다. apply 라는 method 를 만들어서 사용하는데, 이에 대한 definition 은 다음과 같이 작성한다.
private Employee apply(Map<String, Object> patchPayload, Employee tempEmployee) {
ObjectNode employeeNode = objectMapper.convertValue(tempEmployee, ObjectNode.class);
ObjectNode patchNode = objectMapper.convertValue(patchPayload, ObjectNode.class);
employeeNode.setAll(patchNode);
return objectMapper.convertValue(employeeNode, Employee.class);
}method 설명에 앞서 ObjectMapper 에 대하여 알아야 한다. ObjectMapper 는 Jackson library 의 핵심 class 중 하나로, Java object 와 JSON 간의 변환(serialization/deserialization)을 담당한다.
또한 ObjectNode 는 Jackson 의 JsonNode 하위 class 로, immutable(불변) object 인 JsonNode 를 mutable 하게 사용하기 위하여 ObjectNode 를 사용한다.
method 흐름은 다음과 같다. 우선 tempEmployee 와 patchPayload 를 모두 objectMapper.convertValue() method 를 사용하여 ObjectNode type 으로 변환하고, setAll() method 를 통하여 key-value 쌍으로 구성된 field 를 employeeNode 에 overwrite 한다.
마지막으로, employeeNode 를 Employee class type 으로 변환하여 return 하는 구조이다. 이렇게 되면 최종적으로는 request body 로 전달한 JSON format 의 data 가 말그대로 patch 되어 DB 에 update 된다.
test 해보면 성공적으로 결과가 response 된 것을 확인할 수 있다.
Update Controller - Delete an Existing Employee
@DeleteMapping("/employees/{employeeId}")
public String deleteEmployee(@PathVariable int employeeId) {
Employee tempEmployee = employeeService.findById(employeeId);
if (tempEmployee == null) { throw new RuntimeException("Employee id not found - " + employeeId); }
employeeService.deleteById(employeeId);
return "Deleted employee id - " + employeeId;
}Delete 는 간단하다. path variable 로 전달받은 employeeId 값으로 findById() method 를 사용하여 해당 object 를 deleteById method 를 이용하여 삭제하는 방식이다.
test 해보면 성공적으로 삭제가 된 것을 알 수 있다.

실제 DB table 에서도 잘 적용이 된 모습을 볼 수 있다.