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 MethodEndpointCRUD ActionImpl
GET/api/employeesRead a list of employeesO
GET/api/employees/{employeeId}Read a single employeeX
POST/api/employeesCreate a new employeeX
PUT/api/employeesUpdate an existing employeeX
DELETE/api/employees/{employeeId}Delete an existing employeeX

์ฒซ ๋ฒˆ์งธ 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 ์—์„œ๋„ ์ž˜ ์ ์šฉ์ด ๋œ ๋ชจ์Šต์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.