재사용성이 높은 자바 코드를 작성하려면
재사용성이 높고 보기에도 예쁜 코드를 작성해야 한다라는 얘기를 많이 듣긴 하지만 어떻게 하야 재사용성이 높고 예쁜 코드를 작성하는 것인지 막연할 것이다. 추상적이긴 하지만 아마도 읽기 쉽고 이해하기 편하며 유지보수가 용이한 코드를 말하는 것일 것이다. 이 글에서 재사용성이 높은 코드를 작성함에 있어 개발자들이 쉽게 시작할 수 있는 8가지 방법에 대해 얘기해 보고자 한다.
재사용성이 높은 코드를 작성한다는 것은 개발자들에게 매우 중요하게 요구되는 기술중의 하나라서 많은 엔지니어들이 반드시 알아야 하는 덕목이라 할 수 있다. 근래 마이크로서비스아키텍처(MSA)가 보편화 된 개념으로 자리잡고 널리 적용하고 있는 추세이다. 이때 각각의 단위 서비스는 크기가 작고 효율적으로 구성되도록 해야 있으므로 높은 퀄리티로 코드를 작성할 필요까지는 없다라고 말하는 경향이있다. 하지만 마이크로서비스도 나중에 매우 큰 규모로 성장할 수 있으며 이 경우 코드를 읽고 이해하는데 소요되는 시간이 최초로 작성했을 때보다 10배 이상은 많아질 것이다.
만약 처음부터 코드가 예쁘게 잘 작성되지 않았다면 버그를 잡거나 새로운 기능을 추가하는 데 상당한 노력이 필요할 수 있다. 일부 극단적인 경우에는 기존에 개발했던 코드를 모두 폐기하고 처음부터 새로 개발하는 경우도 생길 수 있다. 이런 일이 발생하면 시간도 낭비이긴 하지만 담당했던 개발자는 비난을 면치 못하거나 심한 경우에는 업무가 사라질 위험에 처할 수도 있다.
자 이제부터 재사용성이 높고 예쁜 코드를 작성하기 위해 널리 활용할 만한 8가지 가이드라인에 대해 소개하고자 한다.
재사용성이 높은 자바코드를 작성하기 위한 8가지 가이드라인
- 코드의 작성 룰(rule)을 정의하라.
- API를 문서화 하라.
- 표준적인 네이밍 컨벤션(code naming convention)을 따르라.
- 응집력(Cohesive)이 높은 클래스와 메소드로 작성하라.
- 클래스 끼리는 되로록 커플링(Coupling)을 낮춰라.
- SOLID 원칙 유지하고 준수하라.
- 디자인 패턴을 적절하게 사용하라.
- 처음부터 새로 만들려고 하지 마시라.
코드 작성 규칙/룰을 정의하라
재사용이 높은 코드를 작성하기 위한 첫번째 단계는 팀단위로 코딩 표준을 정의하는 것이다. 그렇지 않으면 머지않아 코드가 매우 복잡하고 지저분해 지는 것을 목도하게 될 것이다. 팀 내에서 이런 규칙이 정해져 있지 않으면 코드 리뷰 회의를 하더라도 의미없는 시간이 될 수 있다. 그리고 개발해야 할 코드가 있다면 기본적인 코드 설계방법도 정해 놓는 것도 좋다.
코딩 표준과 설계방법을 정했다면 코딩 가이드라인을 정해보자. 코딩 가이드라인은 아래와 같이 코딩할 때의 규칙을 설정한다.
- Code naming
- 클래스와 매소드의 라인 분량(Class and method line quantity)
- 예외 처리(Exception handling)
- 패키지 구조(Package structure)
- 개발언어 및 버전(Programming language and version)
- 프레임워크, 툴, 라이브러리
- 코드 테스트 표준
- 코드 Layer(controller, service, repository, domain, etc.)
코딩 규칙을 정하고 동의했다면 팀 전체가 이를 준수하기 위해 서로 노력을 해야 한다. 만약 팀이 동의하지 않았다면 작성되는 코드가 표준을 준수하면서 건강하고 재사용이 높도록 유지되길 바라는 것은 어불성설일 것이다.
API 문서화
서비스를 만들고 이 서비스가 API 형태로 노출될 때 다른 개발자가 쉽게 이해하고 사용할 수 있도록 그 API를 문서화 해 놓아야 한다.
API는 마이크로서비스 구조에서 매우 평범하게 활용된다. 따라서, 내가 만드는 프로젝트에 대해 잘 모르는 다른팀들이 API 문서를 보고 이해할 수 있도록 해야 한다. 만약 API 문서화가 잘 되어 있지 않다면 중복 개발하는 코드가 많아질 것이다. 새로운 개발자의 경우 기존에 개발되어 있음에도 불구하고 새로운 API 메소드를 만들 가능성이 높다는 것을 명심해야 한다.
따라서, API를 문서화 하는 것은 매우 중요하다. 이와 동시에 소스코드 문서화를 너무 많이 한다고 해서 그만큼 가치가 있는 것은 아니다. API로 가치가 있을만한 코드에 대해 적절하게 문서화하면 된다. 예를 들어, API로 인한 비즈니스 오퍼레이션(Business operation), 파라미터, 리턴객체 등을 문서에 포함하면 된다.
코드 네이밍 컨벤션 표준 준수
이상한 약어(acronyms)보다 단순하면서 설명적인 코드명이 훨씬 낫다. 일반적으로 친숙하지 않는 약어를 볼 때면 이 용어가 무엇을 의미하는지 알지 못할 것이다.
따라서, Ctr 와 같은 약어를 사용하지 말고 Customer로 작성하라는 것이다. 이렇게 하는 것이 명확하면서도 더 의미가 있다. Ctr 약어는 control, contract, customer, 등등에 대한 약어로 잘 못 인식될 수도 있기 때문이다.
또한, 프로그래밍 언어만의 네이밍 컨벤션을 사용하는 것이 좋다. 예를 들어, 자바의 경우 JavaBeans 네이밍 컨벤션이라는 게 있다. 이는 단순하며 모든 자바 개발자들은 이해하고 있어야 한다. 다음은 자바에서 클래스, 메소드, 변수, 패키지 이름을 짓는 방법이다.
- Classes, PascalCase: CustomerContract
- Methods and variables, cameCase: customerContract
- Packages, all lowercase: service
응집력 높은 클래스와 메소드 작성
응집력이 높은 코드는 한가지 일(task)을 매우 잘 수행한다. 이런 클래스, 메소드를 작성하는 것은 비록 단순한 개념이긴 하지만 경험이 많은 개발자들도 잘 따르지 않는 경향이 있다. 이렇게 되면 초거대 만능 클래스(ultra-responsible classes)를 생산하게 된다. 이 말은, 너무 많은 일을 하는 클래스를 만든다는 것이다. 이런 클래스를 god class라고 부르기도 한다.
코드의 응집력을 높이려면, 각 클래스, 메소드가 한가지 일을 잘 하도록 코드를 잘게 쪼개는 방법을 터득해야 한다. saveCustomer라는 메소드를 작성할 경우 이 메소드는 “고객 저장” 이라는 하나의 동작만을 하도록 만들어야 한다는 것이다. 즉, 같은 메소드에서 고객을 업데이트하거나 삭제하는 동작을 해서는 안된다는 것이다.
이와 비슷하게 CustomerService라는 클래스가 있다면 여기에는 Customer와 관련된 기능들만을 가지고 있어야 한다. 만약 CustomerService클래스에 product domain에 해당하는 메소드가 있다면 이 메소드를 ProductService 클래스로 이동시켜야 한다.
CustomerService 클래스에 product operation 관련 메소드를 직접 만들지 말고 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 메소드는 상품을 등록할 뿐만 아니라 customer를 수정하고 삭제도 한다. 즉 이 메소드는 한번에 여러가지 동작을 수행한다.
- registerProduct 메소드는 찾기 어려울 수 있다. 이로 인해 필요할 경우 개발자들은 이와 동일한 메소드를 중복해서 개발할 가능성이 높아진다.
- registerProduct 메소드는 도메인 설정이 잘 못 됐다. CustomerPurchaseService에서는 상품(product)을 등록하지 말아야 한다.
- saveCustomerPurchase 메소드는 상품관련 오퍼레이션이 정의되어 있는 외부 클래스를 이용하지 않고 private 메소드를 호출하고 있다.
위 코드에서 무엇이 잘 못 됐는지 어느정도 인지했으니 응집력이 높은 코드로 재작성 해보자. registerProduct 메소드를 해당 도메인(ProductService)으로 이동 시키면 코드는 찾기 쉬워지며 재사용성도 높아질 것이다.
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 메소드를 만들고 여기에는 customer purchase를 저장하는 한 가지 작업만을 수행하도록 했다. 또한 registerProduct의 역할을 ProductService 클래스로 이동 시켰다. 이렇게 하면 기대 했던 것처럼 두 클래스 모두 응집력이 높아진다.
클래스 간의 낮은 커플링(Coupling)
커플링이 높은 코드는 너무 많은 외부 의존도로 인해 유지보수하기가 힘들어 진다. 클래스는 의존도가 높으면 높을 수록 커플링 또한 높아진다.
소프트웨어 아키텍처의 디커플링
이 디커플링 개념은 소프트 아키텍처 상에서도 적용된다. 예를 들어, 마이크로서비스 아키텍처도 서비스들을 디커플링 시키는 것을 목표로 한다. 특정 마이크로서비스가 다른 많은 마이크로서비스와 서로 연결이 되어 있다면 이 서비스는 커플링이 높다라고 말할 수 있다.
코드 재사용을 높이기 위한 좋은 접근법은 시스템과 코드의 의존성을 가능한 한 최소한으로 만드는 것이다. 서비스와 코드는 서로 통신을 해야 하므로 어느 정도의 커플링은 항상 있을 수밖에 없다. 하지만 핵심은 가능한 한 이런 서비스들을 독립적으로 만드는 것이다.
아래 코드는 커플링이 높은 클래스를 예로 보여 준다.
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…
}
CustomerOrderService는 다른 서비스 클래스들과 커플링이 높다는 것을 알 수 있다. 의존도가 높으면 클래스의 코드가 길어지게 될 수밖에 없다. 이는 코드를 테스트하기 힘들게 하거나 유지보수를 어렵게 만든다.
앞서 설명했듯이, 클래스를 분리시켜 의존도가 낮은 형태로 서비스로 만드는 것이 좋은 방법이다. 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…
}
리팩토링 후 CustomerOrderService와 다른 클래스들은 단위 테스트가 훨씬 수월해질 뿐만 아니라 유지보수도 편해진다. 클래스가 특정 영역에 특화되고 간결해지면 질수록 새로운 기능을 구현하기 쉬워지며 버그가 있더라도 수정하기가 편해진다.
SOLID 원칙 준수
SOLID는 객체지향 프로그래밍에서 다섯가지 디자인 원칙(Design Principle)을 나타내는 약어로, 이들 원칙은 소프트웨어 유지보수를 용이하게 하고 유연하며 쉽게 이해할 수 있도록 하는 것을 목표로 한다. 아래는 이들 각각의 원칙들을 간략하게 설명한다.
- Single Responsibility Principle (SRP): 클래스는 하나의 책임(responsibility)과 목적(purpose)을 유지하면서 그 기능을 캡슐화 시켜야 한다. 이 원칙은 클래서의 응집도를 높여주고 한가지 작업에 집중하도록 하며 관리를 용이하게 한다.
- Open-Closed Principle (OCP): 소프트웨어 엔티티(classes, modules, methods, etc)들은 기능 확장에는 열려 있지만 내부의 수정/변경에는 닫혀 있어야 한다. 코드를 설계할 때 새로운 기능과 동작은 기존 코드의 수정 없이 추가할 수 있어야 하며 변경으로 인해 발생하는 영향은 최소화하고, 코드의 재사용성은 높이도록 해야 한다.
- Liskov Substitution Principle (LSP): superclass의 객체를 subclass 객체로 대체 하더라도 프로그램의 정상적 동작에는 영향을 주지 않아야 한다. 다른 말로 하면, Base 클래스의 인스턴스는 상속받은 클래스의 인스턴스를 이용해 대체가 가능해야 하며 이렇게 하더라도 프로그램의 동작은 일관되게 유지해야 한다는 것이다.
- Interface Segregation Principle (ISP): 클라이언트는 사용하지 않는 인터페이스들에 의존적이지 않도록 해야 한다. 이 원칙은 규모가 큰 인터페이스를 작고 구체적인 인터페이스로 쪼개도록 권고한다. 이렇게 함으로써 클라이언트는 관련이 있는 인터페이스에만 의존적이 되도록 할 수 있다. 또한 이렇게 함으로써 느슨한 커플링(Loosely Coupling)을 높이고 불필요한 의존성을 제거할 수 있다.
- Dependency Inversion Principle (DIP): 상위 레벨의 모듈은 하위 레벨의 모듈에 의존적이면 안된다. 둘 다 모두 제 3의 추상화(abstraction)를 만들고 여기에 의존적이 되도록 만들어야 한다. 이 원칙은 추상화(인터페이스 또는추상화 클래스)를 활용하여 상위 레벨의 모듈을 하위 레벨의 상세한 구현 단으로부터 분리 시키도록 한다. 이는 클래스가 구체적인 구현체에 의존하기 보다 추상화에 의존하도록 만들어야 한다는 개념을 강화 시킨다. 이렇게 함으로써 시스템을 더욱 유연하고 테스트가 쉬우며 유지 보수가 용이하도록 만들어준다.
이와 같이 SOLID 원칙을 준수한다면 개발자들은 더욱 모듈화 되고 유지 보수가 용이하며 확장 가능성이 높은 코드를 작성할 수 있을 것이다. 또한, 이해하기 쉽고, 테스트와 수정이 용이하며 더욱 견고하고 변화에 대한 적응력이 높은 소프트웨어 시스템이 되도록 코드 작성에 도움을 준다.
디자인 패턴의 적절한 활용
디자인 패턴은 다양한 코딩 상황을 겪으면서 숙련되고 경험이 많은 개발자들에 의해 만들어 졌다. 적절하게 사용하면 디자인 패턴은 코드의 재사용성을 높여준다.
뿐만 아니라 디자인 패턴을 이해하고 있으면 코드를 읽고 이해하는 능력도 향상시켜 준다. 나아가 내부적으로 활용되는 디자인 패턴을 볼 수 있는 능력이 된다면 JDK의 소스코드도 명확하게 이해할 수 있을 것이다.
디자인 패턴이 강력하다 하더라도 만능이 될 수는 없다. 따라서 이들 디자인 패턴을 활용하는 데는 신중을 기해야 한다. 예를 들어, 단지 알고 있다는 이유 만으로 디자인 패턴을 적용하는 것은 잘못된 방법이라 할 수 있다. 디자인 패턴을 잘못된 상황에 적용하면 코드를 더욱 복잡하게 하고 유지 관리도 어렵게 만들 수 있다. 그러나, 상황에 맞는 유스케이스에 디자인 패턴을 적용하면 코드를 더욱 유연하고 확장성 있게 만들어 준다.
아래는 객체지향 프로그래밍에서 활용되는 디자인 패턴을 간략하게 설명한다. 자세한 내용을 이해하기를 원한다면 개별 패턴과 관련된 인터넷 자료를 검색해서 참고하기 바란다.
생성할 때 활용하는 패턴(Creational Patterns)
- Singleton: 클래스의 인스턴스가 하나만 생성되도록 하며 전역에서 접근할 수 있는 방법을 제공한다.
- Factory method: 객체를 생성하기 위한 인터페이스를 정의하며 이를 상속받은 클래스가 어떤 객체를 생성할지 결정한다.
- Abstract factory: 연관된 객체를 생성하기 위한 인터페이스를 제공한다.
- Builder: 파라미터가 복잡한 객체를 생성할 때 정해진 규격에 얽매이지 않고 느슨한 방법으로 객체 생성을 가능하게 한다.
- Prototype: 기존에 있던 객체를 복제해서 새로운 객체를 생성한다.
구조적 패턴(Structural Patterns)
- Adapter: 특정 클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환한다.
- Decorator: 특정 객체에 가능한 동작을 동적으로 추가한다.
- Proxy: 특정 객체에 대한 접근을 제어하기 위해 대리자 역할을 제공한다.
- Composite: 그룹으로 구성되는 여러 개의 객체를 하나의 객체로 다룰 수 있도록 한다.
- Bridge: 구현체로부터 추상화를 분리시킨다.
동작과 관련된 패턴(Behavioral Pattern)
- Observer: 객체들 사이에 1:N 관계를 형성하도록 한다. 객체의 상태가 변경되면 관계를 맺고 있는 다른 객체들이 변경내용을 자동으로 전달받을 수 있도록 한다.
- Strategy: 관련된 알고리즘을 캡슐화하며 런타임에 어떤 알고리즘을 활용할지 결정하도록 한다.
- Template: Base클래스에 알고리즘의 템플랫을 정의하고 특정 부분은 상속받은 클래스가 정의하도록 한다.
- Command: Request를 객체 형태로 캡슐화하고 이를 파리미터로 넘겨 주면서 Request를 생성할 수 있도록 한다.
- State: 객체의 내부 상태가 변경되면 동작도 바뀌도록 할 수 있다.
- Iterator: 집합적으로 구성된 객체의 각 앨리먼트 내부 구조를 노출시키지 않으면서 순차적으로 접근하는 방법을 제공한다.
- Chain of responsibility: Request를 일련의 Handler Chain으로 통과시키면서 특정 Handler가 Request가 처리될 수 있도록 한다.
- Mediator: 여러 객체가 서로 어떻게 연동해야 하는지를 객체로 캡슐화 시킨다. 이렇게 하면 이들 객체들간에 Loosely Coupling이 될 수 있도록 만들어 준다.
- Visitor: 특정 객체가 알고리즘을 활용할 경우 이 알고리즘을 분리해 내어 제 3의 visitor객체로 이동시킨다.
여기 나열된 대자인 패턴 모두를 자세하게 알아둘 필요까지는 없다. 패턴이라는 것이 있다는 것과 어떤 상황에서 각 패턴을 적용하는지 알아 두는 것만으로도 충분하다. 이정도만 해도 프로그래밍할 때 적절한 디자인패턴을 선택할 수 있을 것이다.
새로 만들지 마시라
많은 회사들이 특별한 이유없이 표준이라고 하면서 자체 프레임워크를 만들어 적용한다. 구글이나 마이크로소프트 같이 규모가 큰 회사가 아니라면 대부부은 자체적으로 프레임워크를 만들 필요는 없다. 중소기업이 자체 솔루션을 만들어 이들 큰 회사들이나 오픈소스들과 겨뤄본다고 하지만 이길 가능성이 낮다.
불필요하게 새로 만드는 일을 벌이지 말고 활용 가능한 툴을 이용하는 것이 훨씬 낫다. 이렇게 하면 개발자들의 커리어에도 도움이 된다. 왜냐하면 회사 내부에서 사용되지 밖에서는 전혀 사용될 가능성이 없는 프레임워크를 배울 필요가 없기 때문이다.
결론…
효율적이면서 향후에도 유지보수가 용이한 소프트웨어가 되도록 재상용성을 높여주는 주요 코딩 원칙들을 이해하고 적용하는 것은 매우 중요하다. 많은 개발자들이 몸소 추상화, 캡슐화, 관심영역 분리(Separation of Concerns), 표준화, 문서화 등을 익힌다면 시간을 절약하고 중복개발을 줄이며 코드의 품질 또한 높일 수 있을 것이다.
디자인 패턴은 일반적으로 마주치는 설계 이슈에 대해 검증된 해결책을 제공하므로 코드의 재사용성 관점에서 중요한 역할을 한다. 늘 하는 얘기이지만 High Cohesion과 Low Coupling은 소프트웨어 컴포넌트들의 재사용성을 높임과 동시에 자기 완결적(Self-contained)이며 서로간의 의존도를 최소화 되도록 만들어 준다. 또한 SOLID 원칙을 준수하면 모듈화를 높여주고 쉽게 확장 가능한 코드가 되도록 하여 다른 프로젝트와 쉽게 통합될 수 있도록 만든다.
재사용가능한 코드를 작성하으로써 개발자들은 개발 생산성을 높인다든가 동료들과의 협업을 원활하게 하여 개발 기간 단축과 같은 혜택을 누릴 수 있다. 궁극적으로, 코드 재성용성을 위한 주요 원칙들은 개발자들로 하여금 Scalable하고 Adaptable하며 Future-proof한 소프트웨어 시스템을 만들 수 있도록 한다.