-
지속 가능한 소프트웨어를 위한 코딩 방법(1)주니어 개발자 2020. 2. 26. 21:39
서론
1. 소프트웨어의 기능이 점차 복잡해지면서 개발자를 더 많이 투입
2. 하지만 현실은 프로젝트에 투입된 개발자는 많아졌지만 소프트웨어를 개발하는 속도와 기능 추가 속도는 비례하지 않는다.
3. 새로운 기능을 추가하거나 버그 수정하면 다른 기능에서 버그가 새롭게 생겨나 상황이 더욱 나빠질 수 있습니다.
4. 즉 개발자가 버그를 관리하지 못하면서 소프트웨어는 신뢰성을 잃고, 사용자는 하나 둘 떠나기 시작한다.
5. 그래서 소프트웨어의 신뢰성을 높이기 위해서 개발자들은 소스코드를 이해하기 쉽게 만드려고 합니다.
6. 의식의 흐름대로 자연스럽게 읽혀서 소프트웨어가 어떤 동작을 하는지 쉽게 이해되는 코드가 좋다.
7. 이 글은 소프트웨어의 복잡성을 줄이기 위한 정리이다.
DRY 원칙
1. 하나의 기능이 여러 곳에 퍼져있으면 유지 보수하기가 어렵다.
2. 마침 그 기능에 버그가 있어서 고쳤는데, 실수로 다른 중복 코드는 고치는데 잊어버렸다.
3. 즉 다른 사람이 개발하거나 혹은 개발한 지 오래되어 다른 중복 코드를 잊을 수 있다.
4. 그래서 Do not Repeat Yourself. 즉 중복 코드를 만들지 말고 남이 작성한 것(이미 만들어진 것)을 쓰라고 한다.
5. 하지만 이 원칙을 잘못 이해하여 기능을 너무 잘게 분해하는 경우가 있다.
-
클래스가 너무 많이 만들어져서 클래스들 간에 의존성이 높아지고나 또는 메서드를 너무 잘게 쪼개서 코드를 이해하는데 의식의 흐름을 방해하는 코드들. 메서드 추상화의 정도가 일정치 않아 발생
-
아래의 코드에서 PurchaseService 클래스의 purchase 메서드의 코드를 분석한다고 가정해봅시다.
-
purchase 메서드의 기능을 분석하기 위해서는 수많은 매서드들을 확인해야 합니다.
-
특히 아래 주석부터 코드 분석을 하고, 다시 돌아오면 기존의 context 가 유지되지 않을 겁니다.
public class DepositService { public ApiResponse deposit(Long depositId){ // 이 메서드를 한번 찾아가보자. 그리고 다시 돌아오면 context가 유지됩니까? Product product = this.getProduct(depositId); ....// return ApiResponse.of(result); } private Product getProduct(Long depositId){ return productService.getProduct(depositId); } } public class ProductService { public Product getProduct(Long depositId){ Product product = getValidProduct(depositId); return product; } public Product getValidProduct(Long depositId){ ProductEntity productEntity = productRepository.findById(depositId).orElseThrow(...); this.convert(productEntity); return depositId; } private Product convert(ProductEntity productEntity){ Product product = new Product(); product.productId = productEntity.productId; /// get. set.. return product } }
- 메서드의 이름이 너무 모호한 경우. 코드를 분석하거나 사용할 때 항상 기능에 대해서 의심을 품는 상황이 발생
-
아래 코드의 priceService.process() 같은 메서드가 대표적입니다. 메서드 이름의 추상화가 너무 심하게 되어있네요.
public PriceEntity getPriceEntityById(Long priceId){ PriceEntity priceEntity = priceRepository.findById(priceId); // 어떻게(How) 프로세싱하겠다는 내용을 메서드, 클래스에서 알 수 없습니다. return priceService.process(priceEntity); }
1. 하나의 메소드는 행위 하나와 같다. 그래서 너무 작은 단위의 메소드를 만드는 건 좋지 않습니다.
2. 메소드 추상화의 크기가 너무 커서도 너무 작아서도 안됩니다.
3. 의미 있는 작업을 하는 매서드를 생성하기
4. 메소드의 기능은 메소드 이름과 일치해야한다.
잘못된 메소드인지 의심하는 방법은?
- 메서드의 이름은 updateShippingStatus()인데, 행위는 업데이트를하지 않고 삭제하는 메서드
- 메서드의 이름이 process()라서 무엇을 하는지 정확한 행위가 드러나지 않는 메서드
- 메서드 이름이 updateShippingStatus()인데, 배송 정보를 업데이트하면서 동시에 업데이트 완료 알림을 보내는 메서드
- 알림을 보내는 행위는 따로 분리하면 좋다. 알림을 보내는 행위와 배송정보를 업데이트하는 행위가 다르기 때문에
메서드의 이름을 의미 있게 만들다 보면 메서드 이름이 길어지는 경우는?
- updateShippingStatusAndSendNotificationToVendor() Q1. updateShippingStatus와 sendNotificationToVendor라는 두 가지의 행위가 메서드 이름에 표현되어야 할 만큼 매우 중요한가? A1-1. 네 두 행위가 매우 중요합니다. (Q2 이동) A1-2. 아니요 updateShippingStatus 만 중요한 행위이므로 메서드를 updateShippingStatus()로 변경합니다. (끝.) Q2. 두 행위 모두 중요하다면, 행위를 서로 분리해서 메서드를 분리하면 어떨까? A2. updateShippingStatus()와 sendNotificationToVendor()로 분리하고 이 둘을 호출하는 메서드를 하나 만듭니다.
클래스 이름을 메타포(은유)를 사용하는 방법
- 배달부 메타포를 사용한다. - 배달부는 물건을 배달하고 난 뒤, 시스템에 배달완료를 입력을 하고 수신인에게 알림을 보냅니다. - 배달부 메타포를 이용해서 updateShippingStatus()를 deliver()로, sendNotificationToVendor()를 alertDelivered()를 사용하는 건 어떨까요? public class PostMan() { public void afterDelivery(){ //.. package.delivered(); vendor.alertDelivered(); //.. } }
직교성 (Orthogonality)
각각의 클래스들은 서로 공통되는 특성이 없어야 합니다. 공통점이 없다는 성질은 앞에서 이야기한 DRY 원칙과 비슷합니다.
1. 함수나 메서드들은 다른 함수나 메서드에 영향이 있어서는 안됩니다.
2. 그래서 응집성이 있고, 독립적인 코드를 작성해야합니다.
3. 이 두 가지 조건(응집, 독립)을 만족할 때 직교성이 있다고 하고, 코드를 격리하고 쉽게 유지보수할 수 있습니다.
예를 들어서 매개변수를 하나 더 추가해보거나 혹은 리턴 객체의 클래스 타입을 바꿔봅시다. 가장 이상적인 것은 메서드를 호출하는 클래스와 시그니처가 변경되는 클래스 하나만 변경 되는거죠.
그런데 앞서 설명한 DepositService.java의 convert() 메서드에 인자 Long makeId를 하나 더 추가해봅시다. 데이터 모델에 따라서 다르겠지만, 꽤 많은 메서드들을 변경해야 할 것 같습니다. 기능이 복잡해서 여러 클래스들이 복잡하게 얽혀 있고, 직교성이 없다면 꽤 많은 클래스들이 수정되어야 하고, 변경된 코드에 버그가 새롭게 생길 수 있습니다.
직교성과 Spring @Transactional
이 직교성을 만족하는 대표적인 코드가 스프링의 @Transactional입니다.
이 어노테이션은 AOP로 구현된 대표적인 JDBC의 트렌젝션 모듈입니다. JDBC Connection의 begin, commit, rollback에 대한 기능을 비즈니스 코드와 분리해서 AOP 프로그래밍된 코드입니다.
개발자는 @Transactional이라는 애너테이션만 선언하고, 코드 내부에서는 트렌젝션 관련 기능을 신경 쓰지 않습니다. 트렌젝션 코드가 비즈니스 코드의 접점은 오직 @Transactional 어노테이션 하나입니다. 그리고 서로의 기능은 완전히 분리되어 있습니다. 비즈니스 코드의 내용을 변경하더라도 트렌잭션 코드는 변경하지 않아도 됩니다. 혹은 반대의 경우도 마찬가지입니다. 그래서 두 코드는 직교성을 갖고 있습니다.
좀 더 자세한 동작을 보고 싶으신 분은 org.springframework.transaction.interceptor.TransactionIntercep tor과 org.springframework.transaction.interceptor.TransactionAspectSu pport를 참고하시길 바랍니다.
'주니어 개발자' 카테고리의 다른 글
Clean Code - 5장 형식 맞추기 (0) 2022.04.02 Clean Code - 4장 주석 (0) 2022.03.12 Clean Code - 3장 함수 (0) 2022.03.12 Clean Code - 2장 의미 있는 이름 (0) 2022.02.26 Clean Code - 1장 깨끗한 코드 (0) 2022.02.20 -