재사용 가능한 자바 코드를 작성하는 가이드라인 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 customer
        productService.registerProduct(customerPurchase.getProduct());
    }

}
public class ProductService {

   public void registerProduct(Product product) {
         // Performs logic for product in the domain of the customer…
    }
}

 

여기서 saveCustomerPurchase가 하는 일은 단 하나, 고객 구매 저장이다. 또한 registerProduct에 대한 책임을 ProductService 클래스에 위임했는데, 이로써 두 클래스 모두 응집력이 높아졌다. 이제 각 클래스와 메서드는 우리가 기대하는 작업을 수행한다.
 

클래스 분리

강하게 결합된 코드에는 종속성이 너무 많아서 유지 관리하기가 어렵다. 클래스에 종속성(정의된 클래스의 수)이 많을수록 그 클래스는 더 강하게 결합된다.
 

소프트웨어 아키텍처의 분리

이 결합 개념은 소프트웨어 아키텍처 맥락에서도 사용된다. 예를 들어 마이크로서비스 아키텍처에도 서비스 분리라는 목표가 있다. 다수의 다른 마이크로서비스와 연결된다면 그 마이크로서비스는 강하게 결합된 마이크로서비스가 된다.
 
코드 재사용을 위한 최선의 접근 방법은 시스템과 코드의 상호 의존성을 최소화하는 것이다. 서비스와 코드는 통신을 해야 하므로 어느 정도의 결합은 항상 존재한다. 핵심은 이러한 서비스를 최대한 독립적으로 만드는 것이다.
 
강하게 결합된 클래스의 예를 들면 다음과 같다.

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…
}

 

CustomerService는 다수의 다른 서비스 클래스와 강하게 결합된다. 종속성이 많다는 것은 클래스에 많은 코드 라인이 필요하다는 것을 의미한다. 결과적으로 코드를 테스트하고 유지 관리하기가 더 어렵게 된다.
 
그보다 더 나은 접근 방법은 이 클래스를 종속성이 적은 여러 서비스로 분리하는 것이다. 다음과 같이 CustomerService를 여러 서비스로 분할해서 결합을 낮춰보자.

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…
}

리팩터링 후에 CustomerService 및 다른 클래스는 단위 테스트하기가 훨씬 더 쉽고 유지 관리하기도 더 쉽다. 클래스가 더 전문화되고 간결할수록 새 기능을 더 쉽게 구현할 수 있다. 버그가 있다면 버그를 수정하기도 더 쉽다.
 
SOLID 원칙에 따르기
SOLID는 객체 지향 프로그래밍(OOP)의 5가지 설계 원칙을 나타내는 약어다. 이 원칙의 목표는 소프트웨어 시스템의 유지관리 용이성과 유연성을 높이고 이해하기 쉽도록 만드는 것이다. 각 원칙을 간단히 설명하면 다음과 같다.
 
단일 책임 원칙(SRP) : 클래스는 하나의 책임 또는 목적을 갖고 그 책임을 캡슐화해야 한다. 이 원칙은 높은 응집력을 촉진하고 클래스의 초점과 관리 편의성을 유지하는 데 도움이 된다.
 
개방-폐쇄 원칙(OCP) : 소프트웨어 엔티티(클래스, 모듈, 메서드 등)는 확장에 개방적이고 수정에 폐쇄적이어야 한다. 기존 코드를 수정하지 않고 새 기능 또는 동작을 추가할 수 있도록 코드를 설계해서 변경의 영향을 줄이고 코드 재사용을 촉진해야 한다.
 
리스코프 치환 원칙(LSP) : 슈퍼클래스의 객체는 프로그램의 정확성에 지장을 주지 않으면서 서브클래스의 객체로 대체가 가능해야 한다. 즉, 기본 클래스의 모든 인스턴스를 파생 클래스의 모든 인스턴스로 대체할 수 있고 프로그램의 동작이 일관적으로 유지되도록 해야 한다.
 
인터페이스 분리 원칙(ISP) : 클라이언트가 사용하지 않는 인터페이스에 의존하도록 강제해서는 안 된다. 이 원칙은 클라이언트가 관련된 인터페이스에만 의존할 수 있도록 큰 인터페이스를 더 작고 구체적인 인터페이스로 나눌 것을 권장한다. 이렇게 하면 느슨한 결합이 촉진되고 불필요한 종속성을 피할 수 있다.
 
의존성 역전 원칙(DIP) : 상위 모듈이 하위 모듈에 의존하면 안 된다. 두 모듈 모두 추상화에 의존해야 한다. 이 원칙은 추상화(인터페이스 또는 추상 클래스)를 사용해서 하위 구현 세부 사항으로부터 상위 모듈을 분리할 것을 권장한다. 클래스는 구체적인 구현보다 추상화에 의존해야 한다는 개념을 장려하는 원칙으로, 시스템의 유연성을 높이고 테스트와 유지보수를 용이하게 해준다.
 
개발자는 이와 같은 SOLID 원칙에 따름으로써 더 모듈화되고 유지 관리가 용이하고 확장 가능한 코드를 만들 수 있다. 이러한 원칙은 더 쉽게 이해, 테스트, 수정할 수 있는 코드를 만드는 데 도움이 되며 결과적으로 더 견고하고 적응성 높은 소프트웨어 시스템으로 이어진다.
 
가능한 부분에 설계 패턴 사용
설계 패턴은 많은 코딩 상황을 겪은 숙련된 개발자들이 만들었다. 설계 패턴을 잘 사용하면 코드 재사용에 도움이 된다.
 
또한 설계 패턴을 이해하면 코드를 읽고 이해하는 능력도 향상된다. 기반 설계 패턴을 볼 수 있으면 JDK의 코드도 더 명확하게 보이게 된다.
 
설계 패턴은 강력하지만 만능은 아니므로 설계 패턴을 사용할 때는 신중을 기해야 한다. 예를 들어 단순히 설계 패턴을 알고 있다는 이유만으로 설계 패턴을 사용하는 것은 실수다. 설계 패턴을 엉뚱한 상황에 사용하면 코드가 더 복잡해지고 유지 관리하기도 어려워진다. 그러나 적절한 사용 사례에 설계 패턴을 적용하면 확장을 위한 코드의 유언성을 높일 수 있다.
 
객체 지향 프로그래밍의 설계 패턴을 간단히 요약하면 다음과 같다.
 
생성 패턴
싱글톤(Singleton) : 클래스가 인스턴스를 하나만 갖고 이 인스턴스에 대한 전역 액세스를 제공하도록 한다.
팩토리 메서드(Factory method) : 객체 생성을 위한 인터페이스를 정의하지만 인스턴스화할 클래스를 서브클래스가 결정할 수 있도록 한다.
추상 팩토리(Abstract factory) : 관련되거나 종속된 객체 군(family)을 만들기 위한 인터페이스를 제공한다.
빌더(Builder) : 복잡한 객체의 구조를 표현에서 분리한다.
프로토타입(Prototype) : 기존 객체를 클론해서 새 객체를 만든다.
 
구조 패턴
어댑터(Adapter) : 클래스의 인터페이스를 클라이언트가 예상하는 다른 인터페이스로 변환한다.

데코레이터(Decorator) : 객체에 동적으로 동작을 추가한다.
프록시(Proxy) : 다른 객체에 대한 액세스를 제어하기 위해 그 객체의 대리자 또는 자리표시자를 제공한다.
컴포지트(Composite) : 객체 그룹을 하나의 객체로 취급한다.
브리지(Bridge) : 추상화와 구현을 분리한다.
 
행동 패턴
옵저버(Observer) : 객체 간에 일대다 종속성을 정의한다. 한 객체의 상태가 변경되면 그 객체의 모든 종속 항목에 알림이 전달되고 자동으로 업데이트된다.

전략(Strategy) : 관련 알고리즘을 캡슐화하고 런타임에 선택 가능하도록 한다.

템플릿 메서드(Template method) : 기본 클래스의 알고리즘 골격을 정의해서 서브클래스에서 구체적인 구현 세부 사항을 제공할 수 있도록 한다.
커맨드(Command) : 요청을 객체로 캡슐화해서 다양한 요청으로 클라이언트를 매개변수화할 수 있도록 한다.
상태(State) : 객체 내부 상태가 변경될 때 객체가 동작을 변경할 수 있도록 한다.
이터레이터(Iterator) : 기반 표현을 노출하지 않으면서 집계 객체의 요소에 순차적으로 액세스할 수 있는 방법을 제공한다.
책임 체인(Chain of responsibility) : 요청이 처리될 때까지 객체가 잠재적인 핸들러 체인을 따라 요청을 전달할 수 있도록 한다.
미디에이터(Mediator) : 객체 집합이 상호작용하는 방식을 캡슐화해서 객체 집합 간의 느슨한 결합을 촉진하는 객체를 정의한다.
비지터(Visitor) : 알고리즘을 별도의 비지터 객체로 옮기는 방법으로 알고리즘과 그 알고리즘이 작동하는 객체를 분리한다.
 
모든 설계 패턴을 배우고 외울 필요는 없다. 패턴이 있다는 것을 알고 각 패턴이 하는 일에 대해 대략 감을 잡는 정도로 충분하다. 그러면 프로그래밍 시나리오에서 적합한 설계 패턴을 선택할 수 있게 된다.
 
이미 있는 기능을 다시 만들지 말 것
많은 기업이 여전히 명확한 이유 없이 내부 프레임워크 사용 표준을 채택한다. 구글, 마이크로소프트와 같은 빅테크가 아닌 한 대부분 내부 프레임워크를 만들 필요가 없다. 중소기업이 오픈소스 또는 빅테크 기업과 경쟁해서 그들보다 다 나은 솔루션을 개발할 수는 없다.
 
이미 있는 것을 다시 만들어서 불필요한 일을 하기보다는 있는 툴을 사용하는 편이 더 낫다. 개발자 경력 관점에서도 그 회사 밖에서는 사용할 일이 없을 프레임워크를 배울 필요가 없으므로 더 좋다.
 
예를 들어 철저한 테스트를 거쳤고 일반적으로 사용되는 지속성 프레임워크인 하이버네이트(Hibernate)가 있다. 전에 필자가 일했던 한 회사에서는 하이버네이트에 비해 기능이나 견고성이 떨어짐에도 불구하고 지속성을 위해 자체 내부 프레임워크를 사용하기로 결정했는데, 이 내부 프레임워크의 확장과 유지 관리를 위해 아무런 실익 없이 부가적인 작업이 발생했다.
 
따라서 필자는 시장에서 폭넓게 사용 가능하고 인기 있는 기술과 툴을 사용할 것을 권장한다. 유능한 개발자들이 몇 년에 걸쳐 협업해서 제품을 개발하고 개선하는 오픈소스와 같은 수준의 품질로 프레임워크를 개발하기는 불가능하다. 또한 많은 경우 대기업이 오픈소스 프로젝트를 지원하면서 프로젝트의 품질에 대한 확신을 더 높여 주기도 한다.
 
결론
코드 재사용성을 위한 주요 원칙을 이해하고 적용하는 것은 효율적이고 유지 관리가 용이한 소프트웨어 시스템을 구축하는 데 있어 매우 중요하다. 개발자는 추상화, 캡슐화, 관심사의 분리, 표준화 및 문서화를 수용함으로써 재사용 가능한 구성요소를 만들어 시간을 절약하고 중복을 줄이고 코드 품질을 개선할 수 있다.
 
설계 패턴은 코드 재사용성에서 중요한 역할을 하며, 일반적인 설계 문제에 대한 검증된 해결 방법을 제공한다. 응집력과 느슨한 결합은 구성요소의 자립성과 최소 의존성을 보장하고 재사용성을 높인다. SOLID 원칙은 다양한 프로젝트에 손쉽게 통합할 수 있는 확장 가능한 모듈형 코드를 촉진한다.
 
개발자는 재사용 가능한 코드를 작성함으로써 생산성 증대, 협업 강화, 개발 주기 가속화를 포함한 수많은 혜택을 얻을 수 있다. 재사용 가능한 코드는 더 빠른 프로젝트 반복과 손쉬운 유지 관리를 가능하게 해주며 기존 솔루션을 활용할 수 있게 해준다. 궁극적으로, 코드 재사용성의 주요 원칙은 개발자에게 확장성과 적응성이 뛰어나고 미래에 대비된 소프트웨어 시스템을 구축할 수 있는 역량을 제공한다.


문제가 될 시 삭제하겠습니다.
Powered by Blogger.