gony-dev 님의 블로그

4. 서비스 리팩터링 본문

Goorm x Kakao Project/1회차 프로젝트

4. 서비스 리팩터링

minarinamu 2024. 12. 28. 01:50

지난 번에는 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