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 ์์๋ ์ ์ ์ฉ์ด ๋ ๋ชจ์ต์ ๋ณผ ์ ์๋ค.