재사용 가능한 자바 코드를 작성하는 가이드라인 8가지
재사용 가능한 코드 작성하기는 모든 소프트웨어 개발자가 갖춰야 할 핵심적인 기량이며, 엔지니어라면 누구나 코드 재사용을 최대화하는 방법을 알아야 한다. 요즘 개발자들은 마이크로서비스가 태생적으로 작고 효율적이므로 고품질 코드 작성에 크게 신경을 쓸 필요가 없다는 핑계를 자주 사용한다. 그러나 마이크로서비스라 해도 상당히 커질 수 있고, 코드를 읽고 이해하는 데 필요한 시간이 처음 작성된 시점보다 10배 이상 늘어날 수 있다.
처음부터 코드를 잘 작성하지 않으면 버그를 해결하거나 새로운 기능을 추가할 때 훨씬 더 많은 작업이 필요하다. 극단적인 사례지만 필자는 애플리케이션을 통째로 버리고 새 코드로 처음부터 다시 시작하는 경우도 여러 번 봤다. 이런 경우가 발생하면 시간이 낭비될 뿐만 아니라 개발자가 비난의 대상이 되고 일자리를 잃을 수도 있다.
이 기사에서는 재사용 가능한 자바 코드를 작성하기 위해 충분히 검증된 8가지 가이드라인을 소개한다.
재사용 가능한 자바 코드 작성을 위한 8가지 가이드라인
코드의 규칙 정의
API 문서화
표준 코드 명명 규칙에 따를 것
응집력 있는 클래스와 메서드 쓰기
클래스 분리
SOLID 원칙에 따르기
가능한 부분에 설계 패턴 사용
이미 있는 기능을 다시 만들지 말 것
코드의 규칙 정의
재사용 가능한 코드를 작성하기 위한 첫 번째 단계는 팀과 함께 코드 표준을 정의하는 것이다. 이 작업을 하지 않으면 코드가 이내 난잡해진다. 또한 팀 합의가 없으면 코드 구현에 관한 쓸데없는 토론도 빈번하게 발생한다. 소프트웨어로 해결하고자 하는 문제에 대한 기본 코드 설계도 결정해야 한다.
표준과 코드 설계를 확보하면 코드 가이드라인을 규정할 차례다. 코드 가이드라인에 따라 코드의 규칙이 결정된다.
코드 명명
클래스 및 메서드 라인 분량
예외 처리
패키지 구조
프로그래밍 언어 및 버전
프레임워크, 툴, 라이브러리
코드 테스트 표준
코드 레이어(컨트롤러, 서비스, 리포지토리, 영역 등)
코드에 대한 규칙이 합의되면 코드를 검토하고 코드가 재사용 가능하도록 잘 작성되었는지 확인할 책임을 팀 전체에 부여할 수 있다. 팀 합의가 이뤄지지 않으면 견고하고 재사용 가능한 표준에 따라 코드가 작성되도록 할 방법이 없다.
API 문서화
서비스를 만들어서 API로 노출할 때는 신규 개발자가 쉽게 이해하고 사용할 수 있도록 API를 문서화해야 한다.
API는 마이크로서비스에서 매우 보편적으로 사용되므로 여러분의 프로젝트에 대해 잘 모르는 다른 팀도 API 문서를 읽고 이해할 수 있어야 한다. API가 제대로 문서화되지 않으면 코드 반복이 발생할 가능성이 더 높아진다. 신규 개발자라면 기존 API와 중복되는 또 다른 API를 만들 수 있다.
따라서 API 문서화는 매우 중요한 일이다. 동시에 지나친 문서화도 코드에서 별다른 가치를 창출하지 못한다. API에서 가치 있는 코드만 문서화해야 한다. 예를 들어 API의 비즈니스 작업, 매개변수, 반환 객체 등을 설명한다.
표준 코드 명명 규칙에 따를 것
뜻을 알 수 없는 약어보다는 간단하고 설명적인 코드 이름이 훨씬 더 좋다. 익숙하지 않은 코드베이스에서 약어가 나오면 필자는 대부분의 경우 그 뜻을 모른다.
따라서 Ctr이라는 약어 대신 Customer를 사용하라. 명확하고 의미도 분명하다. Ctr은 contract, control, customer 등 수많은 단어의 약어가 될 수 있다!
또한 사용 중인 프로그래밍 언어의 명명 규칙에 따라야 한다. 예를 들어 자바의 경우 자바빈즈 명명 규칙이 있다. 간단하며, 모든 자바 개발자가 이해하는 규칙이다. 자바에서 클래스, 메서드, 변수, 패키지를 명명하는 방법은 다음과 같다.
클래스는 파스칼케이스 : CustomerContract
메서드와 변수는 캐멀케이스 : customerContract
패키지는 모두 소문자 : service
응집력 있는 클래스와 메서드 쓰기
응집력 있는 코드는 맡은 한 가지 일을 아주 잘 한다. 응집력 있는 클래스와 메서드를 쓴다는 개념 자체는 간단하지만 숙련된 개발자도 잘 지키지 않는 경우가 많다. 결과적으로 책임이 너무 많은 클래스, 즉 너무 많은 일을 하는 클래스를 만들게 된다. 이 같은 클래스를 갓(god) 클래스라고도 한다.
코드를 응집력 있게 만들려면 각 클래스와 메서드가 한 가지를 잘 수행하도록 코드를 분할하는 방법을 알아야 한다. saveCustomer라는 메서드를 만든다면 이 메서드는 고객을 저장하는 한 가지 동작만 해야지, 고객을 업데이트하고 삭제하는 작업까지 하면 안 된다.
마찬가지로, CustomerService라는 클래스에는 고객에 속한 기능만 있어야 한다. CustomerService 클래스에 제품 영역의 작업을 수행하는 메서드가 있다면 그 메서드를 ProductService 클래스로 옮겨야 한다.
CustomerService 클래스에 제품 작업을 수행하는 메서드를 두는 대신 CustomerService 클래스에서 ProductService를 사용해서 필요한 메서드를 호출할 수 있다.
이 개념을 더 정확히 이해하기 위해 먼저 응집력이 없는 클래스의 예부터 살펴보자.
public class CustomerPurchaseService {
public void saveCustomerPurchase(CustomerPurchase customerPurchase) {
// Does operations with customer
registerProduct(customerPurchase.getProduct());
// update customer
// delete customer
}
private void registerProduct(Product product) {
// Performs logic for product in the domain of the customer…
}
}
이 클래스의 문제는 무엇일까?
saveCustomerPurchase 메서드는 제품을 등록하는 것 외에 고객을 업데이트하고 삭제하는 작업도 한다. 이 메서드는 하는 일이 너무 많다.
registerProduct 메서드는 찾기가 어렵다. 따라서 이와 비슷한 메서드가 필요한 개발자는 이 메서드와 중복되는 메서드를 만들 가능성이 높다.
registerProduct 메서드의 영역이 잘못됐다. CustomerPurchaseService는 제품을 등록하는 작업을 하면 안 된다.
saveCustomerPurchase 메서드는 제품 작업을 수행하는 외부 클래스를 사용하는 대신 비공개 메서드를 호출한다.
코드의 문제점을 알았으니, 이제 응집력 있게 다시 작성할 수 있다. registerProduct 메서드를 올바른 영역인 ProductService로 옮긴다. 이렇게 하면 코드를 검색하고 재사용하기가 훨씬 더 쉬워진다. 또한 이 메서드는 CustomerPurchaseService 내에 갇혀 있지 않게 된다.
public class CustomerPurchaseService {private ProductService productService;public CustomerPurchaseService(ProductService productService) {this.productService = productService;}public void saveCustomerPurchase(CustomerPurchase customerPurchase) {// Does operations with customerproductService.registerProduct(customerPurchase.getProduct());}}public class ProductService {public void registerProduct(Product product) {// Performs logic for product in the domain of the customer…}}
클래스 분리
강하게 결합된 코드에는 종속성이 너무 많아서 유지 관리하기가 어렵다. 클래스에 종속성(정의된 클래스의 수)이 많을수록 그 클래스는 더 강하게 결합된다.소프트웨어 아키텍처의 분리
이 결합 개념은 소프트웨어 아키텍처 맥락에서도 사용된다. 예를 들어 마이크로서비스 아키텍처에도 서비스 분리라는 목표가 있다. 다수의 다른 마이크로서비스와 연결된다면 그 마이크로서비스는 강하게 결합된 마이크로서비스가 된다.코드 재사용을 위한 최선의 접근 방법은 시스템과 코드의 상호 의존성을 최소화하는 것이다. 서비스와 코드는 통신을 해야 하므로 어느 정도의 결합은 항상 존재한다. 핵심은 이러한 서비스를 최대한 독립적으로 만드는 것이다.
강하게 결합된 클래스의 예를 들면 다음과 같다.
public class CustomerOrderService {private ProductService productService;private OrderService orderService;private CustomerPaymentRepository customerPaymentRepository;private CustomerDiscountRepository customerDiscountRepository;private CustomerContractRepository customerContractRepository;private CustomerOrderRepository customerOrderRepository;private CustomerGiftCardRepository customerGiftCardRepository;// Other methods…}
public class CustomerOrderService {private OrderService orderService;private CustomerPaymentService customerPaymentService;private CustomerDiscountService customerDiscountService;// Omitted other methods…}public class CustomerPaymentService {private ProductService productService;private CustomerPaymentRepository customerPaymentRepository;private CustomerContractRepository customerContractRepository;// Omitted other methods…}public class CustomerDiscountService {private CustomerDiscountRepository customerDiscountRepository;private CustomerGiftCardRepository customerGiftCardRepository;// Omitted other methods…}