Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
Tags
- 티스토리챌린지
- codebuild
- mapping
- sqs
- rds
- kakao
- CodeCommit
- jpa
- ec2
- spring
- 백엔드
- Redis
- Docker
- 개발자
- Spring Boot
- aws
- backenddeveloper
- orm
- goorm
- QueryDSL
- CICD
- 자격증
- 오블완
- backend
- MSA
- serverless
- codedeploy
- s3
- DynamoDB
- 스터디
Archives
- Today
- Total
gony-dev 님의 블로그
4. 서비스 리팩터링 본문
지난 번에는 Maven에서 Gradle로 프로젝트를 변경하는 과정을 진행해보았다.
이번에는 내가 맡은 account-service 모듈을 리팩터링하는 과정을 살펴보겠다.
piggymetrics 레거시 코드들의 특징은 다음과 같다.
전체적으로 Lombok을 사용하지 않아 코드로 직접 구현하는 귀찮은 과정을 진행하였고,
오류 발생 시 클라이언트에게 반환하는 Exception도 제대로 구현되어 있지 않는 등 수정할 부분들이 많아 보였다.(오히려 좋아)
하나씩 뜯어 고쳐보겠다.
AccountController
Controller 클래스의 코드 리팩터링 부분은 다음과 같다.
1. 패키지별 API의 분리
2. 원시 Mapping 사용
이들을 수정하면 다음과 같아진다.
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1/account")
public class AccountController {
private final AccountService accountService;
@GetMapping
public String hello() {
return "hello";
}
@PreAuthorize("#oauth2.hasScope('server') or #name.equals('demo')")
@GetMapping("/{name}")
public ResponseEntity<String> getAccountByName(@PathVariable String name) {
return ResponseEntity.ok(accountService.findByName(name));
}
@GetMapping("/current")
public ResponseEntity<String> getCurrentAccount(Principal principal) {
return ResponseEntity.ok(accountService.findByName(principal.getName()));
}
@PutMapping("/current")
public void saveCurrentAccount(Principal principal, @Valid @RequestBody AccountReqDto accountReqDto) {
accountService.saveChanges(principal.getName(), accountReqDto);
}
@PostMapping
public ResponseEntity<String> createNewAccount(@Valid @RequestBody UserReqDto userReqDto) {
accountService.create(userReqDto);
return ResponseEntity.ok("account Created!");
}
}
AccountService
기존 코드
@Service
public class AccountServiceImpl implements AccountService {
private final Logger log = LoggerFactory.getLogger(getClass());
@Autowired
private StatisticsServiceClient statisticsClient;
@Autowired
private AuthServiceClient authClient;
@Autowired
private AccountRepository repository;
/**
* {@inheritDoc}
*/
@Override
public Account findByName(String accountName) {
Assert.hasLength(accountName);
return repository.findByName(accountName);
}
/**
* {@inheritDoc}
*/
@Override
public Account create(User user) {
Account existing = repository.findByName(user.getUsername());
Assert.isNull(existing, "account already exists: " + user.getUsername());
authClient.createUser(user);
Saving saving = new Saving();
saving.setAmount(new BigDecimal(0));
saving.setCurrency(Currency.getDefault());
saving.setInterest(new BigDecimal(0));
saving.setDeposit(false);
saving.setCapitalization(false);
Account account = new Account();
account.setName(user.getUsername());
account.setLastSeen(new Date());
account.setSaving(saving);
repository.save(account);
log.info("new account has been created: " + account.getName());
return account;
}
/**
* {@inheritDoc}
*/
@Override
public void saveChanges(String name, Account update) {
Account account = repository.findByName(name);
Assert.notNull(account, "can't find account with name " + name);
account.setIncomes(update.getIncomes());
account.setExpenses(update.getExpenses());
account.setSaving(update.getSaving());
account.setNote(update.getNote());
account.setLastSeen(new Date());
repository.save(account);
log.debug("account {} changes has been saved", name);
statisticsClient.updateStatistics(name, account);
}
}
서비스 클래스도 마찬가지로 Lombok의 부재로 생성자 주입을 직접 해주고 있었다.
그리고 3가지의 커다란 문제가 있는데 이는 다음과 같다.
1. Exception의 부재
@Override
public Account findByName(String accountName) {
Assert.hasLength(accountName);
return repository.findByName(accountName);
}
- 위와 같은 메서드일 경우 findByName에서 나온 이름을 갖는 데이터가 없다면 그에 맞는 Exception을 반환해야 한다.
하지만 존재하지 않으면 오류를 잡아내기도 힘들고, 서비스 전체에 영향을 미치게 될 수도 있다. - 이를 해결하기 위해 도메인별 Exception 패키지를 만들어 각 상황에 맞는 Exception을 커스터마이징할 수 있다.
2. 엔티티 클래스 그대로 반환
- 메서드들을 보면 Account 엔티티 자체를 반환하는 경우들을 볼 수 있다.
- 이는 설계 측면에서 아주 잘못되었는데 엔티티 클래스가 그대로 외부에 노출된다면 민감한 정보가 보여질 수도 있고,
후에 데이터베이스 구조 변경 시에 의존성이 커져 유지보수에 어려움을 겪을 수 있다. - 이를 해결하기 위해서는 DTO를 사용하여 원하는 내용만을 반환하도록 하는 것이 좋다.
3. Account 엔티티 노출
- 메서드들을 보면 서비스 레이어에서 엔티티들을 Setter를 사용해 생성하는 것을 볼 수 있다.
- 이는 레이어드 아키텍쳐 측면에서 SOLID 원칙을 위배한다. 각 레이어는 서로에 대해 최소한의 의존성을 가져야 하기 때문이다.
- 이를 해결하기 위해서는 엔티티 클래스에서 빌더 패턴을 이용하여 생성하는 방식으로 접근할 수 있다.
4. 비동기 이벤트 처리에 대한 부재
- 다른 모듈의 API를 호출할 때, 해당 API의 문제로 서비스 전체가 중단될 수도 있다.
- 이 방법을 해결하려면 이벤트 기반 아키텍쳐를 도입하는 것이 좋다.
- 비동기 처리를 하여 외부 API 호출이 서비스의 주요 흐름에 영향을 미치게 하지 않도록 하는 이점을 생각할 수 있다.
- 해결 방안으로는 RabbitMQ나 Kafka 또는 스프링의 ApplicationEvent를 사용할 수 있다.
개선된 코드
@Slf4j
@RequiredArgsConstructor
@Service
public class AccountService {
private final StatisticsServiceClient statisticsClient;
private final AuthServiceClient authClient;
private final AccountRepository repository;
@Transactional(readOnly = true)
public String findByName(String accountName) {
Account account = validateName(accountName);
return account.getName();
}
public void create(UserReqDto userReqDto) {
validateName(userReqDto.getUsername());
authClient.createUser(userReqDto);
Saving saving = new Saving(0L, Currency.getDefault(), 0L, false,false);
Account account = new Account(userReqDto, saving);
repository.save(account);
log.info("new account has been created: " + account.getName());
}
public void saveChanges(String name, AccountReqDto update) {
Account account = validateName(name);
account.updateAccount(update);
repository.save(account);
log.debug("account {} changes has been saved", name);
statisticsClient.updateStatistics(name, account);
}
private Account validateName(String name) {
return repository.findByName(name)
.orElseThrow(() -> new AccountNotFoundException("Not found: " + name));
}
}
- 이벤트 기반 아키텍쳐는 아직 구현하지 못하여 추후에 추가할 예정이다!
다른 부분은 크게 수정할 곳이 없어 이만 글을 마무리하겠다.
다음 시간에는 해당 모듈을 유레카 서버에 등록하고 gateway로 라우팅해 보겠다.
'Goorm x Kakao Project > 1회차 프로젝트' 카테고리의 다른 글
3. Maven에서 Gradle (0) | 2024.12.18 |
---|---|
2. Spring Cloud MSA (0) | 2024.12.18 |
1. MSA를 사용하는 이유 (1) | 2024.12.13 |