인프런에서 주최하는 Warming-up 클럽 0기 백엔드 스터디에 참여하고 있다.
스터디에 참여하면서 배우게 된 내용을 전체적으로 정리하고, 참여하면서 느낀 부분을 회고해 보고자 한다.
(1) 5일 차 : 2024-02-23(Fri)
1. 클린 코드(Clean code)
1-1. 좋은 코드란?
(1) 작성된 코드라는 것은 기술적 요구사항, 비즈니스적인 요구사항, 기능을 수행하기 위해 실제로 구현한 언어이다. 이러한 클린 코드는 단순해서 읽기 쉽고, 각 역할마다 정해진 하나의 일만 담당하며 따라서 복잡하거나 모호하지 않은 코드를 의미한다.
(2) 개발자는 요구사항을 구현하기 위해 기존의 코드를 읽고 작성한다.
- 현업에서는 기존에 존재하는 수많은 코드를 읽고 이해하며 새로운 요구사항을 구현하는 것 보다는 기존의 코드를 리팩토링하거나 수정하는 일이 많음
- 작성하는 시간보다 읽는 시간이 더 많음
- 팀 단위로 일하는 경우 다른 사람이 작성한 코드를 많이 읽게 된다. 코드를 읽는 것은 필수적이고 피할 수 없는 부분이다.
1-2. 안 좋은 코드가 쌓이면 시간이 지날수록 생산성이 감소한다.
(1) 이전까지의 문제, 하나의 컨트롤러에서 너무 많은 역할을 수행한다.
1-3. 하나의 컨트롤러에서 왜 많은 기능을 담당할 수 없는 걸까?
(1) 클린 코드에 의하면, 함수는 최대한 작게 구성하고 한 가지 일만 수행하는 것이 좋다.
(2) 클래스는 작아야 하며 하나의 책임만을 가져야 한다.
1-4. 현재 작성된 컨트롤러의 일부 메서드를 확인해 보자
@PutMapping("/api/v1/fruit")
public void updateSaleState(@RequestBody FruitSaleStateUpdateRequestDto request) {
String selectSql = "select * from fruit where fruit_id = ?";
boolean isFruitNotExist = jdbcTemplate.query(selectSql, (rs, rowNum) -> 0, request.getId()).isEmpty();
if (isFruitNotExist) {
throw new IllegalStateException("존재하지 않는 과일 정보입니다.");
}
String sql = "update fruit set is_sale = 1 where fruit_id = ?";
jdbcTemplate.update(sql, request.getId());
}
(1) 클라이언트 요청의 엔드포인트에서 넘어온 HTTP Body를 객체로 변환하고 있다.
(2) 유저의 존재 여부에 따른 분기에 따라 예외 처리를 수행한다.
(3) 실제 쿼리를 통해 데이터베이스와 통신한다.
(4) 이 부분을 각자의 역할에 맞게 분리하는 것이 중요하다.
2. 컨트롤러의 많은 역할들을 분리하기(Controller - Service - Repository)
2-1. Layered Architecture (계층형 아키텍처)
- 위와 같이 각 계층이 서로의 역할에 맞게 독립되어 격리되어 있는 구조를 Layered Architecture라고 부른다.
(1) Controller (API 엔드포인트, HTTP 관련 처리, 비즈니스 로직이 시작되는 엔드포인트(인터페이스 역할))
- 클라이언트 요청의 엔드포인트에서 넘어온 HTTP Body를 객체로 변환하고 있다.
- Controller에서는 Service를 사용
(2) Service (핵심 비즈니스 로직 처리, 분기, 예외처리 수행 등)
- 유저의 존재 여부에 따른 분기에 따라 예외 처리를 수행한다.
- Service에서는 Repository를 사용
(3) Repository (데이터베이스와의 직접적인 통신, 데이터베이스 접근 영역)
- 실제 쿼리를 통해 데이터베이스와 통신한다.
(4) 마지막으로 DTO는 계층 간의 정보 전달을 목적으로 사용된다.
2-2. UserController, UserService, UserRepository로 분리
(1) UserController.java
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PutMapping("/user")
public void updateUser(@RequestBody UserUpdateRequestDto request) {
userService.updateUser(request);
}
}
- 컨트롤러 계층에서는 사용자의 이름을 업데이트하기 위한 서비스 계층의 핵심 비즈니스 메서드를 호출하며 요청으로 넘어온 정보도 함께 파라미터로 전달해준다.(Controller → Service)
- 기존 코드에서는 컨트롤러 계층에서 데이터베이스 접근을 위한 JdbcTemplate을 각 계층을 호출할 때마다 계속 넘겨주었는데 이 부분도 데이터베이스와의 통신과 연관되어 있으므로 리포지토리 영역으로 분리시켰다.
(2) UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public void updateUser(UserUpdateRequestDto request) {
if (userRepository.isUserNotExist(request.getId())) {
throw new IllegalStateException("존재하지 않는 회원입니다.");
}
userRepository.updateUserName(request.getName(), request.getId());
}
}
- 사용자 이름을 업데이트하기 위해서, 존재하지 않는 회원인지 이 부분에 대한 예외를 처리하기 위해 리포지토리 영역에서 isUserNotExist를 호출한다. (Service → Repository)
- 존재하는 회원임이 확인되면 다시 리포지토리 영역에서 사용자의 이름을 변경하는 메서드를 호출한다 (Service → Repository)
(3) UserRepository.java
@Repository
@RequiredArgsConstructor
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public boolean isUserNotExist(long id) {
String selectSql = "select * from user where id = ?";
return jdbcTemplate.query(selectSql, (rs, rowNum) -> 0, id).isEmpty();
}
public void updateUserName(String name, long id) {
String sql = "update user set name = ? where id = ?";
jdbcTemplate.update(sql, name, id);
}
}
- 리포지토리 영역에서 사용자의 요청에 따른 데이터베이스와의 통신을 담당한다.
2-3. 사용자 업데이트만이 아닌, 모든 비즈니스 포인트(CRUD)들을 대상으로 Controller - Service - Repository로 분리하자.
(1) UserController.java
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/user")
public void saveUser(@RequestBody UserCreateRequestDto request) {
userService.saveUser(request);
}
@GetMapping("/user")
public List<UserListResponseDto> getAllUsers() {
return userService.findAllUser().stream()
.map(user -> new UserListResponseDto(user.getId(), user.getName(), user.getAge()))
.collect(Collectors.toList());
}
@PutMapping("/user")
public void updateUser(@RequestBody UserUpdateRequestDto request) {
userService.updateUser(request);
}
@DeleteMapping("/user")
public void deleteUser(UserDeleteRequestDto request) {
userService.deleteUser(request);
}
}
(2) UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public void saveUser(UserCreateRequestDto request) {
userRepository.saveUser(request.getName(), request.getAge());
}
public List<User> findAllUser() {
return userRepository.findAllUser();
}
public void updateUser(UserUpdateRequestDto request) {
if (userRepository.isUserNotExist(request.getId())) {
throw new IllegalStateException("존재하지 않는 회원입니다.");
}
userRepository.updateUserName(request.getName(), request.getId());
}
public void deleteUser(UserDeleteRequestDto request) {
if (userRepository.isUserNotExist(request.getName())) {
throw new IllegalStateException("존재하지 않는 회원입니다.");
}
userRepository.deleteUserByUsername(request.getName());
}
}
(3) UserRepository.java
@Repository
@RequiredArgsConstructor
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public void saveUser(String name, int age) {
String sql = "insert into user(name, age) values(?, ?)";
jdbcTemplate.update(sql, name, age);
}
public List<User> findAllUser() {
String sql = "select * from user";
return jdbcTemplate.query(sql, (rs, rowNum) -> {
long id = rs.getLong("id");
String name = rs.getString("name");
int age = rs.getInt("age");
return new User(id, name, age);
});
}
public boolean isUserNotExist(long id) {
String selectSql = "select * from user where id = ?";
return jdbcTemplate.query(selectSql, (rs, rowNum) -> 0, id).isEmpty();
}
public boolean isUserNotExist(String name) {
String selectSql = "select * from user where name = ?";
return jdbcTemplate.query(selectSql, (rs, rowNum) -> 0, name).isEmpty();
}
public void updateUserName(String name, long id) {
String sql = "update user set name = ? where id = ?";
jdbcTemplate.update(sql, name, id);
}
public void deleteUserByUsername(String name) {
String sql = "delete from user where name = ?";
jdbcTemplate.update(sql, name);
}
}
2-4. 결론
(1) 기존에 계층형 구조를 띄지 않고 모든 역할이 한 곳에 모여있던 컨트롤러 코드와, 이제는 계층별로 역할이 명확하게 분리된 코드들(Controller, Service, Repository) 비교해 보면 코드가 이전에 비해 상당히 가독성도 좋아지고 추후 유지보수 측면에서도 훨씬 수월해진 코드를 확인해 볼 수 있다.
3. 개인 회고
(1) 이처럼 계층형 아키텍처가 모든 상황에서 100% 정답은 아니겠지만, 프로덕트의 품질, 비즈니스 측면, 개발자들과의 협업, 추후 요구사항 변경, 코드 리팩토링, 테스트, 유지보수 등 모든 관점에서 봤을 때 계층형 아키텍처 방식의 설계가 매우 효율적인 설계라는 것을 알 수 있었다.
(2) 클린 코드, 오브젝트 등 더 좋은 객체지향의 설계 방법이 어떤 방법인지 더 공부해 보고 싶다.
4. Reference
(1) 관련 레퍼런스는 인프런에서 활동하고 계시는 최태현 강사님의 온라인 강의를 듣고 내용을 정리했습니다.
※ 해당 포스팅에 대해 내용 추가가 필요하다고 생각되면 기존 포스팅 내용에 다른 내용이 추가될 수 있습니다.
개인적으로 공부하며 정리한 내용이기에 오타나 틀린 부분이 있을 수 있으며, 이에 대해 댓글로 알려주시면 감사하겠습니다
'기록, 회고 > InFlearn Warming-up 0기 BE' 카테고리의 다른 글
[6일 차] - 내용 정리, 개인 회고 (0) | 2024.02.23 |
---|---|
[5일 차] - 과제 수행 : 클린 코드, 코드 리팩토링 (0) | 2024.02.21 |
[4일 차] - 과제 수행 : API 개발 연습 (0) | 2024.02.21 |
[4일 차] - 내용 정리, 개인 회고 (0) | 2024.02.21 |
[3일 차] - 과제 수행 : 익명 클래스, 함수형 프로그래밍(람다식), Stream API, 메서드 참조(Method Reference) (4) | 2024.02.20 |
댓글